diff options
Diffstat (limited to 'emms-info-native.el')
-rw-r--r-- | emms-info-native.el | 348 |
1 files changed, 193 insertions, 155 deletions
diff --git a/emms-info-native.el b/emms-info-native.el index f31475f..9f0ed2d 100644 --- a/emms-info-native.el +++ b/emms-info-native.el @@ -25,31 +25,31 @@ ;; 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-ogginfo' or +;; `emms-info-libtag'. ;; -;; To use this method, add ‘emms-info-native’ to -;; ‘emms-info-functions’. +;; To use this method, add `emms-info-native' to +;; `emms-info-functions'. ;; ;; The following file formats are supported: ;; -;; - Vorbis: Ogg Vorbis I Profile, filename extension ‘.ogg’, -;; elementary streams only. Based on xiph.org’s Vorbis I +;; - 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’. +;; `https://xiph.org/vorbis/doc/Vorbis_I_spec.html'. ;; -;; - Opus: Ogg Opus profile, filename extension ‘.opus’, elementary +;; - Opus: Ogg Opus profile, filename extension `.opus', elementary ;; streams only. Based on RFC 7845, see URL -;; ‘https://tools.ietf.org/html/rfc7845.html’. +;; `https://tools.ietf.org/html/rfc7845.html'. ;; ;; - 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'. Based on xiph.org's FLAC format specification, see URL +;; `https://xiph.org/flac/format.html'. ;; -;; - MP3 files with extension ‘.mp3’ and id3v2 tags. All id3v2 +;; - 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’. +;; see URL `https://id3.org'. ;; ;; Format detection is based solely on filename extension, which is ;; matched case-insensitively. @@ -62,10 +62,10 @@ (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 +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 +`emms-info-native--max-peek-size' bytes are needed, an error is signaled. Technically metadata blocks can have almost arbitrary lengths, @@ -76,7 +76,7 @@ exhaustion in case of garbled or malicious inputs.") (defconst emms-info-native--ogg-magic-array [79 103 103 83] - "Ogg format magic capture pattern ‘OggS’.") + "Ogg format magic capture pattern `OggS'.") (defconst emms-info-native--ogg-page-size 65307 "Maximum size for a single Ogg container page.") @@ -84,7 +84,7 @@ exhaustion in case of garbled or malicious inputs.") (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’" + (error "Ogg framing mismatch: expected `%s', got `%s'" emms-info-native--ogg-magic-array last))) (stream-structure-version u8) @@ -104,10 +104,10 @@ exhaustion in case of garbled or malicious inputs.") (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’. +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." +`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)) @@ -123,9 +123,9 @@ 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 +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. +`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 @@ -172,7 +172,7 @@ Note that N can be zero." (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’." +`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)) @@ -180,11 +180,11 @@ PAGE must correspond to (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’. +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’." +`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)) @@ -243,11 +243,11 @@ their comments have almost the same format as Vorbis.") "titlesort" "tracknumber" "year") - "Emms info fields that are extracted from Vorbis comments.") + "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’.") + "Header packet magic pattern `vorbis'.") (defconst emms-info-native--vorbis-headers-bindat-spec '((identification-header struct emms-info-native--vorbis-identification-header-bindat-spec) @@ -263,7 +263,7 @@ header.") last))) (vorbis vec 6) (eval (unless (equal last emms-info-native--vorbis-magic-array) - (error "Vorbis framing mismatch: expected ‘%s’, got ‘%s’" + (error "Vorbis framing mismatch: expected `%s', got `%s'" emms-info-native--vorbis-magic-array last))) (vorbis-version u32r) @@ -289,7 +289,7 @@ header.") last))) (vorbis vec 6) (eval (unless (equal last emms-info-native--vorbis-magic-array) - (error "Vorbis framing mismatch: expected ‘%s’, got ‘%s’" + (error "Vorbis framing mismatch: expected `%s', got `%s'" emms-info-native--vorbis-magic-array last))) (vendor-length u32r) @@ -319,14 +319,14 @@ header.") (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’. +`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." +`emms-info-native--accepted-vorbis-fields' are returned." (let (comments) (dolist (user-comment user-comments) (let* ((comment (cdr (assoc 'user-comment user-comment))) @@ -339,7 +339,7 @@ FIELDs that are listed in (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 +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. @@ -363,11 +363,11 @@ outside itself.") (defconst emms-info-native--opus-head-magic-array [79 112 117 115 72 101 97 100] - "Opus identification header magic pattern ‘OpusHead’.") + "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’.") + "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) @@ -379,7 +379,7 @@ 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’" + (error "Opus framing mismatch: expected `%s', got `%s'" emms-info-native--opus-head-magic-array last))) (opus-version u8) @@ -406,7 +406,7 @@ header.") (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’" + (error "Opus framing mismatch: expected `%s', got `%s'" emms-info-native--opus-tags-magic-array last))) (vendor-length u32r) @@ -450,7 +450,7 @@ header.") "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." +`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 @@ -506,12 +506,12 @@ outside itself.") (defconst emms-info-native--id3v2-magic-array [#x49 #x44 #x33] - "id3v2 header magic pattern ‘ID3’.") + "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) - (error "id3v2 framing mismatch: expected ‘%s’, got ‘%s’" + (error "id3v2 framing mismatch: expected `%s', got `%s'" emms-info-native--id3v2-magic-array last))) (version u8) @@ -522,39 +522,48 @@ outside itself.") (size eval (emms-info-native--checked-id3v2-size 'tag last))) "id3v2 header specification.") -(defconst emms-info-native--id3v2-frame-bindat-spec +(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))) - (payload vec (size))) - "id3v2 frame specification.") + (flags bits (eval (if (= emms-info-native--id3v2-version 2) 0 2)))) + "id3v2 frame header specification.") (defconst emms-info-native--id3v2-frame-to-info - '(("TP1" . "artist") - ("TPE1" . "artist") + '(("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") - ("TALB" . "album") - ("TAL" . "album") - ("TSOA" . "albumsort") - ("TRCK" . "tracknumber") ("TRK" . "tracknumber") - ("TPOS" . "discnumber") - ("TPA" . "discnumber") - ("TDRC" . "year") - ("TDRL" . "year") - ("TYER" . "year") + ("TRCK" . "tracknumber") ("TYE" . "year") - ("TDOR" . "originalyear") - ("TORY" . "originalyear") - ("TOR" . "originalyear")) - "Mapping from id3v2 frame identifiers to info fields.") + ("TYER" . "year")) + "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--id3v2-text-encodings '((0 . latin-1) @@ -565,21 +574,26 @@ outside itself.") (defun emms-info-native--decode-id3v2 (filename) "Read and decode id3v2 metadata from FILENAME. -Return metadata in a list of (FIELD . VALUE) cons cells. See -‘emms-info-native--decode-id3v2-text-frame’ for details." - (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--decode-id3v2-ext-header-size filename))) - (emms-info-native--decode-id3v2-frames filename - offset - (+ tag-size 10) - unsync))) +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." @@ -589,29 +603,28 @@ Return metadata in a list of (FIELD . VALUE) cons cells. See (bindat-unpack emms-info-native--id3v2-header-bindat-spec (buffer-string)))) -(defun emms-info-native--decode-id3v2-ext-header-size (filename) +(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 exceeds -‘emms-info-native--max-peek-size’." +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 (buffer-string)))) + (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. Check the validity of size. -Return the size." - (let (size) - (cond ((eq elt 'tag) - (setq size (emms-info-native--decode-id3v2-size bytes t))) - ((eq elt 'frame) - (if (= emms-info-native--id3v2-version 4) - (setq size (emms-info-native--decode-id3v2-size bytes t)) - (setq size (emms-info-native--decode-id3v2-size bytes nil))))) - (when (or (= size 0) (> size emms-info-native--max-peek-size)) - (error "id3v2 tag or frame size %s is invalid" size)) - size)) +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. @@ -624,10 +637,7 @@ Return the decoded size." (* (expt 2 (* num-bits idx)) elt)) (reverse bytes))))) -(defun emms-info-native--decode-id3v2-frames (filename - begin - end - unsync) +(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 @@ -636,61 +646,87 @@ 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. See -‘emms-info-native--decode-id3v2-text-frame’ for details." - (with-temp-buffer - (set-buffer-multibyte nil) - (insert (emms-info-native--read-id3v2-frames filename - begin - end - unsync)) - (emms-info-native--decode-id3v2-text-frames (buffer-string)))) - -(defun emms-info-native--read-id3v2-frames (filename begin end unsync) - "Read id3v2 frames from FILE. -Start at offset BEGIN and end before offset END. If UNSYNC is t, -reverse unsynchronization. - -Return the frames." - (with-temp-buffer - (set-buffer-multibyte nil) - (insert-file-contents-literally filename nil begin end) - (when unsync - (while (re-search-forward (string 255 0) nil t) - (replace-match (string 255)))) - (buffer-string))) - -(defun emms-info-native--decode-id3v2-text-frames (frames) - "Decode id3v2 text frames from FRAMES. - -Return metadata in a list of (FIELD . VALUE) cons cells. See -‘emms-info-native--decode-id3v2-text-frame’ for details." - (let (comments - (offset 0)) +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 (length frames)) - (let* ((frame (bindat-unpack emms-info-native--id3v2-frame-bindat-spec - frames - offset)) - (comment (emms-info-native--decode-id3v2-text-frame - frame))) - (when comment (push comment comments)) - (cl-incf offset - (+ (bindat-get-field frame 'size) - (if (= emms-info-native--id3v2-version 2) 6 10))))) + (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)))) (error nil)) comments)) -(defun emms-info-native--decode-id3v2-text-frame (frame) - "Identify and decode id3v2 text frame FRAME. -If FRAME’s identifier matches a key in -‘emms-info-native--id3v2-frame-to-info’, return a cons cell -(FIELD . VALUE), where FIELD is the corresponding info field -identifier and VALUE is the decoded text. Otherwise return nil." - (let ((info-id (emms-info-native--id3v2-frame-info-id frame)) - (payload (bindat-get-field frame 'payload))) - (when info-id - (cons info-id (emms-info-native--decode-id3v2-string payload))))) +(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--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-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--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--id3v2-frame-info-id (frame) "Return the emms-info identifier for FRAME. @@ -700,8 +736,9 @@ If there is no such identifier, return nil." (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." +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))) @@ -725,11 +762,12 @@ Supports Ogg Vorbis/Opus, FLAC, and MP3 files." (dolist (field info-fields) (let ((name (intern (concat "info-" (car field)))) (value (cdr field))) - (emms-track-set track - name - (if (eq name 'info-playing-time) - (string-to-number value) - value)))))) + (unless (zerop (length value)) + (emms-track-set track + name + (if (eq name 'info-playing-time) + (string-to-number value) + (string-trim-right value)))))))) (defun emms-info-native--decode-info-fields (filename) "Decode info fields from FILENAME. @@ -750,7 +788,7 @@ strings." This is a naive implementation that relies solely on filename extension. -Return one of symbols ‘vorbis’, ‘opus’, ‘flac’, or ‘mp3’." +Return one of symbols `vorbis', `opus', `flac', or `mp3'." (let ((case-fold-search t)) (cond ((string-match ".ogg$" filename) 'vorbis) ((string-match ".opus$" filename) 'opus) |