[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[elpa] externals/emms 3f83a56d3f 04/42: Decode playing time from Ogg fil
From: |
ELPA Syncer |
Subject: |
[elpa] externals/emms 3f83a56d3f 04/42: Decode playing time from Ogg files |
Date: |
Wed, 1 Nov 2023 15:57:59 -0400 (EDT) |
branch: externals/emms
commit 3f83a56d3f631d86d39c38cda79f8880faaa88f6
Author: Petteri Hintsanen <petterih@iki.fi>
Commit: Petteri Hintsanen <petterih@iki.fi>
Decode playing time from Ogg files
---
emms-info-native.el | 166 +++++++++++++++++++++++++++++++++++++----
test/emms-info-native-tests.el | 60 ++++++++++++++-
2 files changed, 210 insertions(+), 16 deletions(-)
diff --git a/emms-info-native.el b/emms-info-native.el
index b4bf0b9b5b..d571251eaf 100644
--- a/emms-info-native.el
+++ b/emms-info-native.el
@@ -89,6 +89,7 @@ outside itself.")
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
@@ -161,8 +162,8 @@ header.")
(eval (unless (= last 0)
(error "Vorbis version mismatch: expected 0, got %s"
last)))
- (audio-channels u8)
- (audio-sample-rate u32r)
+ (channel-count u8)
+ (sample-rate u32r)
(bitrate-maximum u32r)
(bitrate-nominal u32r)
(bitrate-minimum u32r)
@@ -249,6 +250,7 @@ lower case and VALUE is the decoded value."
(cons (downcase (match-string 1 comment-string))
(match-string 2 comment-string)))))
+
;;;; Opus code
(defconst emms-info-native--opus-headers-bindat-spec
@@ -312,6 +314,7 @@ header.")
(string-to-vector "OpusTags")
"Opus comment header magic pattern.")
+
;;;; Ogg code
(defconst emms-info-native--ogg-page-size 65307
@@ -329,9 +332,9 @@ header.")
last)))
(header-type-flag u8)
(granule-position vec 8)
- (stream-serial-number vec 4)
- (page-sequence-no vec 4)
- (page-checksum vec 4)
+ (stream-serial-number u32r)
+ (page-sequence-no u32r)
+ (page-checksum u32r)
(page-segments u8)
(segment-table vec (page-segments))
(payload vec (eval (seq-reduce #'+ last 0))))
@@ -341,8 +344,63 @@ header.")
(string-to-vector "OggS")
"Ogg format magic capture pattern.")
-(defun emms-info-native--decode-ogg-comments (filename stream-type)
- "Read and decode comments from Ogg file FILENAME.
+(defconst emms-info-native--ogg-crc-table
+ [#x00000000 #x04C11DB7 #x09823B6E #x0D4326D9 #x130476DC
+ #x17C56B6B #x1A864DB2 #x1E475005 #x2608EDB8 #x22C9F00F
+ #x2F8AD6D6 #x2B4BCB61 #x350C9B64 #x31CD86D3 #x3C8EA00A
+ #x384FBDBD #x4C11DB70 #x48D0C6C7 #x4593E01E #x4152FDA9
+ #x5F15ADAC #x5BD4B01B #x569796C2 #x52568B75 #x6A1936C8
+ #x6ED82B7F #x639B0DA6 #x675A1011 #x791D4014 #x7DDC5DA3
+ #x709F7B7A #x745E66CD #x9823B6E0 #x9CE2AB57 #x91A18D8E
+ #x95609039 #x8B27C03C #x8FE6DD8B #x82A5FB52 #x8664E6E5
+ #xBE2B5B58 #xBAEA46EF #xB7A96036 #xB3687D81 #xAD2F2D84
+ #xA9EE3033 #xA4AD16EA #xA06C0B5D #xD4326D90 #xD0F37027
+ #xDDB056FE #xD9714B49 #xC7361B4C #xC3F706FB #xCEB42022
+ #xCA753D95 #xF23A8028 #xF6FB9D9F #xFBB8BB46 #xFF79A6F1
+ #xE13EF6F4 #xE5FFEB43 #xE8BCCD9A #xEC7DD02D #x34867077
+ #x30476DC0 #x3D044B19 #x39C556AE #x278206AB #x23431B1C
+ #x2E003DC5 #x2AC12072 #x128E9DCF #x164F8078 #x1B0CA6A1
+ #x1FCDBB16 #x018AEB13 #x054BF6A4 #x0808D07D #x0CC9CDCA
+ #x7897AB07 #x7C56B6B0 #x71159069 #x75D48DDE #x6B93DDDB
+ #x6F52C06C #x6211E6B5 #x66D0FB02 #x5E9F46BF #x5A5E5B08
+ #x571D7DD1 #x53DC6066 #x4D9B3063 #x495A2DD4 #x44190B0D
+ #x40D816BA #xACA5C697 #xA864DB20 #xA527FDF9 #xA1E6E04E
+ #xBFA1B04B #xBB60ADFC #xB6238B25 #xB2E29692 #x8AAD2B2F
+ #x8E6C3698 #x832F1041 #x87EE0DF6 #x99A95DF3 #x9D684044
+ #x902B669D #x94EA7B2A #xE0B41DE7 #xE4750050 #xE9362689
+ #xEDF73B3E #xF3B06B3B #xF771768C #xFA325055 #xFEF34DE2
+ #xC6BCF05F #xC27DEDE8 #xCF3ECB31 #xCBFFD686 #xD5B88683
+ #xD1799B34 #xDC3ABDED #xD8FBA05A #x690CE0EE #x6DCDFD59
+ #x608EDB80 #x644FC637 #x7A089632 #x7EC98B85 #x738AAD5C
+ #x774BB0EB #x4F040D56 #x4BC510E1 #x46863638 #x42472B8F
+ #x5C007B8A #x58C1663D #x558240E4 #x51435D53 #x251D3B9E
+ #x21DC2629 #x2C9F00F0 #x285E1D47 #x36194D42 #x32D850F5
+ #x3F9B762C #x3B5A6B9B #x0315D626 #x07D4CB91 #x0A97ED48
+ #x0E56F0FF #x1011A0FA #x14D0BD4D #x19939B94 #x1D528623
+ #xF12F560E #xF5EE4BB9 #xF8AD6D60 #xFC6C70D7 #xE22B20D2
+ #xE6EA3D65 #xEBA91BBC #xEF68060B #xD727BBB6 #xD3E6A601
+ #xDEA580D8 #xDA649D6F #xC423CD6A #xC0E2D0DD #xCDA1F604
+ #xC960EBB3 #xBD3E8D7E #xB9FF90C9 #xB4BCB610 #xB07DABA7
+ #xAE3AFBA2 #xAAFBE615 #xA7B8C0CC #xA379DD7B #x9B3660C6
+ #x9FF77D71 #x92B45BA8 #x9675461F #x8832161A #x8CF30BAD
+ #x81B02D74 #x857130C3 #x5D8A9099 #x594B8D2E #x5408ABF7
+ #x50C9B640 #x4E8EE645 #x4A4FFBF2 #x470CDD2B #x43CDC09C
+ #x7B827D21 #x7F436096 #x7200464F #x76C15BF8 #x68860BFD
+ #x6C47164A #x61043093 #x65C52D24 #x119B4BE9 #x155A565E
+ #x18197087 #x1CD86D30 #x029F3D35 #x065E2082 #x0B1D065B
+ #x0FDC1BEC #x3793A651 #x3352BBE6 #x3E119D3F #x3AD08088
+ #x2497D08D #x2056CD3A #x2D15EBE3 #x29D4F654 #xC5A92679
+ #xC1683BCE #xCC2B1D17 #xC8EA00A0 #xD6AD50A5 #xD26C4D12
+ #xDF2F6BCB #xDBEE767C #xE3A1CBC1 #xE760D676 #xEA23F0AF
+ #xEEE2ED18 #xF0A5BD1D #xF464A0AA #xF9278673 #xFDE69BC4
+ #x89B8FD09 #x8D79E0BE #x803AC667 #x84FBDBD0 #x9ABC8BD5
+ #x9E7D9662 #x933EB0BB #x97FFAD0C #xAFB010B1 #xAB710D06
+ #xA6322BDF #xA2F33668 #xBCB4666D #xB8757BDA #xB5365D03
+ #xB1F740B4]
+ "Lookup table for calculating Ogg checksums.")
+
+(defun emms-info-native--decode-ogg-metadata (filename stream-type)
+ "Read and decode metadata from Ogg file FILENAME.
The file is assumed to contain a single stream of type
STREAM-TYPE, which must either `vorbis' or `opus'.
@@ -351,10 +409,13 @@ Return comments in a list of (FIELD . VALUE) cons cells.
See
(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)))
+ (rate (bindat-get-field headers 'identification-header 'sample-rate))
+ (commdata (bindat-get-field headers 'comment-header 'user-comments))
+ (lastpage (emms-info-native--read-and-decode-last-ogg-page filename))
+ (granpos (cdr (assoc 'granule-position lastpage)))
+ (playtime (emms-info-native--decode-ogg-granule-position granpos
rate))
+ (comments (emms-info-native--extract-vorbis-comments commdata)))
+ (nconc comments (when playtime (list (cons "playing-time" playtime))))))
(defun emms-info-native--decode-ogg-packets (filename packets)
"Read and decode packets from Ogg file FILENAME.
@@ -395,7 +456,7 @@ Return the plist from `emms-info-native--decode-ogg-page'."
nil
offset
(+ offset
- emms-info-native--ogg-page-size))
+ emms-info-native--ogg-page-size))
;FIXME: may go over file size
(emms-info-native--decode-ogg-page (buffer-string))))
(defun emms-info-native--decode-ogg-page (bytes)
@@ -437,6 +498,83 @@ Return a structure that corresponds to either
packets)))
(t (error "Unknown stream type %s" stream-type))))
+(defun emms-info-native--read-and-decode-last-ogg-page (filename)
+ "Read and decode the last Ogg page from FILENAME.
+Return the page in bindat type structure."
+ (with-temp-buffer
+ (let* ((length (file-attribute-size (file-attributes filename)))
+ (begin (max 0 (- length emms-info-native--ogg-page-size))))
+ (set-buffer-multibyte nil)
+ (insert-file-contents-literally filename nil begin length)
+ (emms-info-native--decode-last-ogg-page))))
+
+(defun emms-info-native--decode-last-ogg-page ()
+ "Find and return the last valid Ogg page from the current buffer.
+Ensure page synchronization by verifying Ogg page checksum.
+Return the page in bindat type structure. If there is no valid
+Ogg page in the buffer, return nil."
+ (let (page)
+ (goto-char (point-max))
+ (while (and (not page)
+ (search-backward (concat emms-info-native--ogg-magic-pattern)
nil t))
+ (setq page (condition-case nil
+ (emms-info-native--verify-ogg-page)
+ (error nil))))
+ (when (and page
+ (> (logand (cdr (assoc 'header-type-flag page)) #x04) 0))
+ page)))
+
+(defun emms-info-native--verify-ogg-page ()
+ "Verify Ogg page starting from point.
+Unpack page into `emms-info-native--ogg-page-bindat-spec'
+structure and calculate its checksum. Return the page if the
+checksum is correct, or nil if the checksum does not match."
+ (let* ((offset (point))
+ (page (bindat-unpack emms-info-native--ogg-page-bindat-spec
+ (buffer-string)
+ (1- offset)))
+ (num-bytes (bindat-length emms-info-native--ogg-page-bindat-spec
page))
+ (buf (buffer-substring-no-properties offset (+ offset num-bytes)))
+ (checksum (emms-info-native--calculate-ogg-checksum (concat
(substring buf 0 22)
+ [0 0 0 0]
+ (substring buf
26)))))
+ (when (= (cdr (assoc 'page-checksum page)) checksum) page)))
+
+(defun emms-info-native--calculate-ogg-checksum (bytes)
+ "Calculate and return Ogg checksum for BYTES.
+See URL `https://xiph.org/vorbis/doc/framing.html' for details on
+checksum."
+ (let ((crc 0))
+ (dotimes (n (length bytes))
+ (setq crc (logxor (logand (ash crc 8) #xffffffff)
+ (aref emms-info-native--ogg-crc-table
+ (logxor (ash crc -24)
+ (aref bytes n))))))
+ crc))
+
+(defun emms-info-native--decode-ogg-granule-position (vec rate)
+ "Decode Ogg granule position vector VEC for sampling rate RATE.
+Granule position is 64-bit little-endian signed integer counting
+the number of PCM samples per channel. If granule position is
+-1, it was for a partial packet and hence invalid. In that case
+return nil."
+ (let* ((int (emms-info-native--vector-to-integer vec))
+ (pos (emms-info-native--unsigned-to-signed int 64)))
+ (unless (= pos -1) (/ pos rate))))
+
+(defun emms-info-native--vector-to-integer (vec)
+ (apply '+ (seq-map-indexed (lambda (elt idx)
+ (* (expt 2 (* 8 idx)) elt))
+ vec)))
+
+(defun emms-info-native--unsigned-to-signed (num bits)
+ (let ((bitmask (1- (expt 2 bits)))
+ (max (1- (expt 2 (1- bits)))))
+ (if (> num max)
+ (* -1 (1+ (logand (lognot num) bitmask)))
+ num)))
+
+
;;;; FLAC code
(defconst emms-info-native--flac-metadata-block-header-bindat-spec
@@ -513,6 +651,7 @@ Return the comment block data in a vector."
(setq offset end)))
comment-block)))
+
;;;; id3v2 (MP3) code
(defconst emms-info-native--id3v2-header-bindat-spec
@@ -931,6 +1070,7 @@ Return the text as string."
(cdr (assoc (seq-first bytes)
emms-info-native--id3v2-text-encodings)))
+
;;;; EMMS code
(defun emms-info-native (track)
@@ -958,7 +1098,7 @@ 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--decode-ogg-metadata filename stream-type))
((eq stream-type 'flac)
(emms-info-native--decode-flac-comments filename))
((eq stream-type 'mp3)
diff --git a/test/emms-info-native-tests.el b/test/emms-info-native-tests.el
index c9a26f3496..318d966fea 100644
--- a/test/emms-info-native-tests.el
+++ b/test/emms-info-native-tests.el
@@ -29,7 +29,7 @@
(should (equal (emms-info-native--decode-ogg-page bytes)
(list :num-packets 1
:num-bytes 58
- :stream [1 118 111 114 98 105 115 0 0 0 0 1 68 172 0
0 0 0 0 0 128 56 1 0 0 0 0 0 184 1 ])))))
+ :stream [1 118 111 114 98 105 115 0 0 0 0 1 68 172 0
0 0 0 0 0 128 56 1 0 0 0 0 0 184 1])))))
(ert-deftest emms-test-decode-ogg-vorbis-headers ()
(let ((bytes [1 118 111 114 98 105 115 0 0 0 0 1 68 172 0 0 0 0 0 0 128 56 1
0 0 0 0 0 184 1 3 118 111 114 98 105 115 52 0 0 0 88 105 112 104 46 79 114 103
32 108 105 98 86 111 114 98 105 115 32 73 32 50 48 50 48 48 55 48 52 32 40 82
101 100 117 99 105 110 103 32 69 110 118 105 114 111 110 109 101 110 116 41 2 0
0 0 7 0 0 0 102 111 111 61 98 97 114 27 0 0 0 75 101 121 61 206 159 225 189 144
207 135 225 189 182 32 206 164 206 177 225 189 144 207 132 225 189 176 1]))
@@ -52,8 +52,8 @@
(bitrate-minimum . 0)
(bitrate-nominal . 80000)
(bitrate-maximum . 0)
- (audio-sample-rate . 44100)
- (audio-channels . 1)
+ (sample-rate . 44100)
+ (channel-count . 1)
(vorbis-version . 0)
(vorbis . [118 111 114 98 105 115])
(packet-type . 1)))))))
@@ -82,6 +82,59 @@
(opus-version . 1)
(opus-head . [79 112 117 115 72 101 97 100])))))))
+(defun emms-test--decode-last-ogg-page (bytes)
+ (with-temp-buffer
+ (set-buffer-multibyte nil)
+ (insert (concat bytes))
+ (emms-info-native--decode-last-ogg-page)))
+
+(ert-deftest emms-test-decode-last-ogg-page()
+ (let ((valid [#x01 #x02 #x03 #x04 #x4f #x67 #x67 #x53 #x00 #x04 #x00 #x24
#x08 #x01 #x00 #x00 #x00 #x00 #x9c #x39 #x6e #x47 #x40 #x08 #x00 #x00 #x19 #x4e
#xac #xa3 #x01 #x0a #x4f #x67 #x67 #x53 #x31 #x32 #x33 #x34 #x35 #x36])
+ (notlast [#x01 #x02 #x03 #x04 #x4f #x67 #x67 #x53 #x00 #x00 #x00 #x24
#x08 #x01 #x00 #x00 #x00 #x00 #x9c #x39 #x6e #x47 #x40 #x08 #x00 #x00 #x19 #x4e
#xac #xa3 #x01 #x0a #x4f #x67 #x67 #x53 #x31 #x32 #x33 #x34 #x35 #x36])
+ (invalid [#x01 #x02 #x03 #x04 #x4f #x67 #x67 #x53 #x00 #x04 #x00 #x24
#x08 #x01 #x00 #x00 #x00 #x00 #x9c #x39 #x6e #x47 #x40 #x08 #x00 #x00 #x01 #x02
#x03 #x04 #x01 #x0a #x4f #x67 #x67 #x53 #x31 #x32 #x33 #x34 #x35 #x36]))
+ (should (equal (emms-test--decode-last-ogg-page valid)
+ '((payload . [79 103 103 83 49 50 51 52 53 54])
+ (segment-table . [10])
+ (page-segments . 1)
+ (page-checksum . 2745978393)
+ (page-sequence-no . 2112)
+ (stream-serial-number . 1198406044)
+ (granule-position . [0 36 8 1 0 0 0 0])
+ (header-type-flag . 4)
+ (stream-structure-version . 0)
+ (capture-pattern . [79 103 103 83]))))
+ (should (equal (emms-test--decode-last-ogg-page notlast) nil))
+ (should (equal (emms-test--decode-last-ogg-page invalid) nil))))
+
+(ert-deftest emms-test-calculate-ogg-checksum ()
+ (let ((bytes [#x01 #x02 #x03 #x04 #x4f #x67 #x67 #x53 #x00 #x04 #x00 #x24
#x08 #x01 #x00 #x00 #x00 #x00 #x9c #x39 #x6e #x47 #x40 #x08 #x00 #x00 #x19 #x4e
#xac #xa3 #x01 #x0a #x4f #x67 #x67 #x53 #x31 #x32 #x33 #x34 #x35 #x36]))
+ (should (equal (emms-info-native--calculate-ogg-checksum bytes)
445885580))))
+
+(ert-deftest emms-test-decode-ogg-granule-position ()
+ (should (equal (emms-info-native--decode-ogg-granule-position [0 36 8 1 0 0
0 0] 44100)
+ 392))
+ (should (equal (emms-info-native--decode-ogg-granule-position [40 236 178 11
0 0 0 0] 48000)
+ 4089))
+ (should (equal (emms-info-native--decode-ogg-granule-position [255 255 255
255 255 255 255 255] nil)
+ nil)))
+
+(ert-deftest emms-test-vector-to-integer ()
+ (should (equal (emms-info-native--vector-to-integer [0]) 0))
+ (should (equal (emms-info-native--vector-to-integer [127]) 127))
+ (should (equal (emms-info-native--vector-to-integer [255]) 255))
+ (should (equal (emms-info-native--vector-to-integer [0 1]) 256))
+ (should (equal (emms-info-native--vector-to-integer [1 0]) 1))
+ (should (equal (emms-info-native--vector-to-integer [0 128]) 32768)))
+
+(ert-deftest emms-test-unsigned-to-signed ()
+ (should (equal (emms-info-native--unsigned-to-signed 0 8) 0))
+ (should (equal (emms-info-native--unsigned-to-signed 1 8) 1))
+ (should (equal (emms-info-native--unsigned-to-signed 127 8) 127))
+ (should (equal (emms-info-native--unsigned-to-signed 128 8) -128))
+ (should (equal (emms-info-native--unsigned-to-signed 129 8) -127))
+ (should (equal (emms-info-native--unsigned-to-signed 254 8) -2))
+ (should (equal (emms-info-native--unsigned-to-signed 255 8) -1)))
+
(defmacro emms-test-make-flac-data-func (name bytes)
`(defun ,name (offset end replace)
(let ((bytes (seq-concatenate 'vector [102 76 97 67] ,bytes)))
@@ -97,3 +150,4 @@
(should-error (emms-info-native--decode-flac-comment-block
#'emms-test-invalid-flac-block-type))
(should (equal (emms-info-native--decode-flac-comment-block
#'emms-test-valid-flac-block)
[1 2 3 4])))
+
- [elpa] externals/emms updated (cdea73e122 -> 32fd570ed7), ELPA Syncer, 2023/11/01
- [elpa] externals/emms b083c59e18 07/42: Decode playing time from MP3 files, ELPA Syncer, 2023/11/01
- [elpa] externals/emms e501654df6 01/42: Split some functions for easier testing, ELPA Syncer, 2023/11/01
- [elpa] externals/emms 7ce067566f 02/42: Add some tests for Ogg, Opus and FLAC code, ELPA Syncer, 2023/11/01
- [elpa] externals/emms 45245b8b6b 08/42: Decode playing time from FLAC files, ELPA Syncer, 2023/11/01
- [elpa] externals/emms 643284a952 14/42: Streamline Ogg page reading and decoding, ELPA Syncer, 2023/11/01
- [elpa] externals/emms 3f83a56d3f 04/42: Decode playing time from Ogg files,
ELPA Syncer <=
- [elpa] externals/emms 3a7341f660 17/42: Avoid some copying in emms-info-vorbis-extract-comments, ELPA Syncer, 2023/11/01
- [elpa] externals/emms a6d2bbe484 18/42: Remove useless mapconcat, ELPA Syncer, 2023/11/01
- [elpa] externals/emms 68a0dfa9b8 19/42: Add Unicode character to metadata in test files, ELPA Syncer, 2023/11/01
- [elpa] externals/emms fca5f3e7d5 26/42: Use bindat-type in emms-info-flac, ELPA Syncer, 2023/11/01
- [elpa] externals/emms a0c4d715f4 12/42: Use fixed sample rate when decoding Opus granule position, ELPA Syncer, 2023/11/01
- [elpa] externals/emms 813c0058bd 28/42: Use bindat-type in emms-info-spc, ELPA Syncer, 2023/11/01
- [elpa] externals/emms 32b394b2b1 34/42: Change tests to use unibyte strings, ELPA Syncer, 2023/11/01
- [elpa] externals/emms c9dce6b305 36/42: Fix tests, ELPA Syncer, 2023/11/01
- [elpa] externals/emms 93c614dc24 32/42: Fix byte compilation errors, ELPA Syncer, 2023/11/01
- [elpa] externals/emms 83490a7218 38/42: Change all tests to use emms-test- prefix, ELPA Syncer, 2023/11/01