-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
1.2.14: Bugfix: handle waveform for last cue
subed-waveform should now handle the case where the stop time + subed-waveform-preview-msecs-after might extend past the end of the file. * subed/subed-waveform.el (subed-waveform-ffprobe-executable): New. (subed-waveform-file-duration-ms-cache): New. (subed-waveform-ffprobe-duration-ms): New, calculates duration. (subed-waveform-file-duration-ms): New function for caching the duration. (subed-waveform-clear-file-duration-ms-cache): New. (subed-mpv): Add advice around subed-mpv-play-from-file for now; ideally change this to a hook later on. (subed-waveform--image-parameters): Move to a separate function for easier testing. (subed-waveform--make-overlay): Do the calculations in subed-waveform--image-parameters. (subed-waveform--update-bars): Use the actual stop time if needed. * tests/test-subed-waveform.el: New. * Set lexical-binding: t in tests/* files Thanks to rodrigomorales1 and rndusr for bug reports and pull requests! Related: - #68 - #75 - #74
- Loading branch information
Showing
10 changed files
with
412 additions
and
29 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
#+BEGIN_COMMENT | ||
SPDX-FileCopyrightText: 2021 The subed Authors | ||
SPDX-FileCopyrightText: 2021-2024 The subed Authors | ||
|
||
SPDX-License-Identifier: CC0-1.0 | ||
#+END_COMMENT | ||
|
@@ -12,4 +12,7 @@ Please note this shouldn't be taken as a list of copyright holders, | |
nor is it necessarily complete. | ||
|
||
- Random User <[email protected]> (original creator) | ||
- Sacha Chua <[email protected]> | ||
- Sebastian 'seabass' Crane <[email protected]> | ||
- Marcin Borkowski | ||
- Rodrigo Morales |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
;;; subed-waveform.el --- display waveforms in subed buffers -*- lexical-binding: t; -*- | ||
|
||
;; Copyright (C) 2023 Sacha Chua, Marcin Borkowski | ||
;; Copyright (C) 2023-2024 Sacha Chua, Marcin Borkowski, Rodrigo Morales | ||
|
||
;; Author: Sacha Chua <[email protected]>, Marcin Borkowski <[email protected]> | ||
;; Keywords: multimedia | ||
|
@@ -140,6 +140,11 @@ SVG parameters of the displayed bars. Every bar must have a unique | |
:value-type (plist :key-type symbol :value-type string)) | ||
:group 'subed-waveform) | ||
|
||
(defcustom subed-waveform-ffprobe-executable "ffprobe" | ||
"Path to the FFprobe executable used for measuring file duration." | ||
:type 'file | ||
:group 'subed-waveform) | ||
|
||
(defcustom subed-waveform-preview-msecs-before 2000 | ||
"Prelude in milliseconds displaying subtitle waveform." | ||
:type 'integer | ||
|
@@ -244,6 +249,123 @@ WIDTH and HEIGHT are given in pixels." | |
width height) | ||
"[bg][fg]overlay=format=auto,drawbox=x=(iw-w)/2:y=(ih-h)/2:w=iw:h=1:color=#9cf42f")) | ||
|
||
(defvar-local subed-waveform-file-duration-ms-cache nil "If non-nil, duration of current file in milliseconds.") | ||
|
||
(defun subed-waveform-convert-ffprobe-tags-duration-to-ms (duration) | ||
"Return milliseconds as an integer for DURATION. | ||
DURATION must be a string of the format HH:MM:SS.MMMM. | ||
Example: | ||
00:00:03.003000000 -> 3003 | ||
00:00:03.00370000 -> 3004" | ||
(unless (string-match "\\([0-9]\\{2\\}\\):\\([0-9]\\{2\\}\\):\\([0-9]\\{2\\}\\)\\.\\([0-9]+\\)" duration) | ||
(error "The duration is not well formatted.")) | ||
(let ((hour (match-string 1 duration)) | ||
(minute (match-string 2 duration)) | ||
(seconds (match-string 3 duration)) | ||
(milliseconds (match-string 4 duration))) | ||
(+ | ||
(* (string-to-number hour) 3600000) | ||
(* (string-to-number minute) 60000) | ||
(* (string-to-number seconds) 1000) | ||
(* (string-to-number (concat "0." milliseconds)) 1000)))) | ||
|
||
(defun subed-waveform-ffprobe-duration-ms (filename) | ||
"Use ffprobe to get duration of audio stream in milliseconds of FILENAME." | ||
(let ((json | ||
(json-read-from-string | ||
(with-temp-buffer | ||
(call-process | ||
subed-waveform-ffprobe-executable nil t nil | ||
"-v" "error" | ||
"-print_format" "json" | ||
"-show_streams" | ||
"-show_format" | ||
filename) | ||
(buffer-string))))) | ||
;; Check that the file has at least one audio stream. | ||
(when (eq (seq-find | ||
(lambda (stream) | ||
(equal (alist-get 'codec_type stream) "audio")) | ||
(alist-get 'streams json)) | ||
0) | ||
(error "The provided file doesn't have an audio stream.")) | ||
(cond | ||
;; If the file has one stream and it is an audio stream, we can | ||
;; get the duration from format=duration | ||
;; | ||
;; nb_streams equals the number of streams in the media file. | ||
((and (eq (alist-get 'nb_streams (alist-get 'format json)) 1) | ||
(equal (alist-get | ||
'codec_type | ||
(seq-first (alist-get 'streams json))) | ||
"audio")) | ||
(* 1000 (string-to-number | ||
(alist-get 'duration (alist-get 'format json))))) | ||
;; If the file has more than one stream and only one audio | ||
;; stream, return the duration of the audio stream. | ||
((and (> (alist-get 'nb_streams (alist-get 'format json)) 1) | ||
(eq (length (seq-filter | ||
(lambda (stream) | ||
(equal (alist-get 'codec_type stream) "audio")) | ||
(alist-get 'streams json))) | ||
1)) | ||
(cond | ||
((or | ||
(string-match "\\.mkv\\'" filename) | ||
(string-match "\\.webm\\'" filename)) | ||
(subed-waveform-convert-ffprobe-tags-duration-to-ms | ||
(alist-get | ||
'DURATION | ||
(alist-get | ||
'tags | ||
(seq-find | ||
(lambda (stream) | ||
(equal (alist-get 'codec_type stream) "audio")) | ||
(alist-get 'streams json)))))) | ||
(t | ||
(* 1000 | ||
(string-to-number | ||
(alist-get | ||
'duration | ||
(seq-find | ||
(lambda (stream) | ||
(equal (alist-get 'codec_type stream) "audio")) | ||
(alist-get 'streams json)))))))) | ||
;; TODO: Some media files might have multiple audio streams | ||
;; (e.g. multiple languages). When the media file has multiple | ||
;; audio streams, prompt the user for the audio stream. The audio | ||
;; stream selected by the user must be stored in a buffer-local | ||
;; variable so that ffmpeg knows the audio stream from which the | ||
;; waveforms are created. | ||
))) | ||
|
||
(defun subed-waveform-file-duration-ms (&optional filename) | ||
"Return the duration of FILENAME in milliseconds." | ||
(cond | ||
(subed-waveform-file-duration-ms-cache | ||
(when (> subed-waveform-file-duration-ms-cache 0) | ||
subed-waveform-file-duration-ms-cache)) | ||
(subed-waveform-ffprobe-executable | ||
(setq subed-waveform-file-duration-ms-cache | ||
(subed-waveform-ffprobe-duration-ms | ||
(or filename (subed-media-file)))) | ||
(if (> subed-waveform-file-duration-ms-cache 0) | ||
subed-waveform-file-duration-ms-cache | ||
;; mark as invalid | ||
(setq subed-waveform-file-duration-ms-cache -1) | ||
nil)))) | ||
|
||
(defun subed-waveform-clear-file-duration-ms-cache (&rest _) | ||
"Clear `subed-waveform-file-duration-ms-cache'." | ||
(setq subed-waveform-file-duration-ms-cache nil)) | ||
|
||
;; This should eventually be replaced with a hook. | ||
(with-eval-after-load 'subed-mpv | ||
(advice-add 'subed-mpv-play-from-file :after 'subed-waveform-clear-file-duration-ms-cache)) | ||
|
||
(defun subed-waveform--from-file (filename from to width height) | ||
"Returns a string representing the image data in PNG format. | ||
FILENAME is the input file, FROM and TO are time positions, WIDTH | ||
|
@@ -284,44 +406,72 @@ and HEIGHT are dimensions in pixels." | |
(when pos | ||
(format "%.2f%%" (/ (* 100.0 (- pos start)) (- stop start))))) | ||
|
||
(defun subed-waveform--image-parameters (&optional width height) | ||
"Return a plist of media-file, start, stop, width, height. | ||
Use WIDTH and HEIGHT if specified." | ||
(let* ((duration (subed-waveform-file-duration-ms (subed-media-file))) | ||
(start (floor (max 0 (- (subed-subtitle-msecs-start) subed-waveform-preview-msecs-before)))) | ||
(stop | ||
(min | ||
(floor (+ (subed-subtitle-msecs-stop) subed-waveform-preview-msecs-after)) | ||
(or duration most-positive-fixnum))) | ||
(width-ratio | ||
(/ | ||
(* 100.0 (- stop start)) | ||
(- (+ (subed-subtitle-msecs-stop) subed-waveform-preview-msecs-after) start))) | ||
(width (or width (/ (* width-ratio (string-pixel-width (make-string fill-column ?*))) | ||
(face-attribute 'default :height)))) | ||
(height (or height (save-excursion | ||
;; don't count the current waveform towards the | ||
;; line height | ||
(forward-line -1) | ||
(* 2 (line-pixel-height)))))) | ||
(list | ||
:file | ||
(or (subed-media-file) | ||
(error "No media file found")) | ||
:start | ||
start | ||
:stop | ||
stop | ||
:width | ||
width | ||
:height | ||
height))) | ||
|
||
(defun subed-waveform--make-overlay (&optional width height) | ||
"Make an overlay at point for the current subtitle." | ||
(let* ((overlay (make-overlay (point) (point))) | ||
(start (floor (max 0 (- (subed-subtitle-msecs-start) subed-waveform-preview-msecs-before)))) | ||
(stop (floor (+ (subed-subtitle-msecs-stop) subed-waveform-preview-msecs-after))) | ||
(width (/ (* 100.0 (string-pixel-width (make-string fill-column ?*))) | ||
(face-attribute 'default :height))) | ||
(height (save-excursion | ||
;; don't count the current waveform towards the | ||
;; line height | ||
(forward-line -1) | ||
(* 2 (line-pixel-height)))) | ||
(params (subed-waveform--image-parameters width height)) | ||
(image (subed-waveform--from-file | ||
(or (subed-media-file) | ||
(error "No media file found")) | ||
(subed-waveform--msecs-to-ffmpeg start) | ||
(subed-waveform--msecs-to-ffmpeg stop) | ||
width | ||
height)) | ||
(svg (svg-create width height))) | ||
(plist-get params :file) | ||
(subed-waveform--msecs-to-ffmpeg (plist-get params :start)) | ||
(subed-waveform--msecs-to-ffmpeg (plist-get params :stop)) | ||
(plist-get params :width) | ||
(plist-get params :height))) | ||
(svg (svg-create | ||
(plist-get params :width) | ||
(plist-get params :height)))) | ||
(svg-embed svg image "image/png" t | ||
:x 0 :y 0 | ||
:width "100%" :height "100%" | ||
:preserveAspectRatio "none") | ||
(overlay-put overlay 'subed-waveform t) | ||
(overlay-put overlay 'after-string "\n") | ||
(overlay-put overlay 'waveform-start start) | ||
(overlay-put overlay 'waveform-stop stop) | ||
(overlay-put overlay 'waveform-start (plist-get params :start)) | ||
(overlay-put overlay 'waveform-stop (plist-get params :stop)) | ||
(overlay-put overlay 'before-string | ||
(propertize | ||
" " | ||
'display (svg-image svg) | ||
'svg svg | ||
'pointer 'arrow | ||
'keymap subed-waveform-svg-map | ||
'waveform-start start | ||
'waveform-stop stop | ||
'waveform-pixels-per-second (/ width (* 0.001 (- stop start))))) | ||
'waveform-start (plist-get params :start) | ||
'waveform-stop (plist-get params :stop) | ||
'waveform-pixels-per-second (/ (plist-get params :width) | ||
(* 0.001 (- (plist-get params :stop) | ||
(plist-get params :start)))))) | ||
(unless subed-waveform-show-all | ||
(setq subed-waveform--overlay overlay) | ||
(setq subed-waveform--svg svg)) | ||
|
@@ -365,7 +515,8 @@ If POSITION is nil, remove the bar." | |
"Update the bars in OVERLAY." | ||
(setq overlay (or overlay (subed-waveform--get-current-overlay))) | ||
(let* ((start (subed-subtitle-msecs-start)) | ||
(stop (subed-subtitle-msecs-stop)) | ||
(stop (min (subed-subtitle-msecs-stop) | ||
(or (subed-waveform-file-duration-ms) most-positive-fixnum))) | ||
(start-pos (subed-waveform--position-to-percent | ||
start | ||
(overlay-get overlay 'waveform-start) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,3 @@ | ||
;;;; SPDX-FileCopyrightText: 2023 Sacha Chua, Marcin Borkowski | ||
;;;; SPDX-FileCopyrightText: 2023, 2024 Sacha Chua, Marcin Borkowski, Rodrigo Morales | ||
;;;; | ||
;;;; SPDX-License-Identifier: GPL-3.0-or-later |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
;;; subed.el --- A major mode for editing subtitles -*- lexical-binding: t; -*- | ||
|
||
;; Version: 1.2.13 | ||
;; Version: 1.2.14 | ||
;; Maintainer: Sacha Chua <[email protected]> | ||
;; Author: Random User | ||
;; Keywords: convenience, files, hypermedia, multimedia | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.