Skip to content

Commit

Permalink
1.2.14: Bugfix: handle waveform for last cue
Browse files Browse the repository at this point in the history
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
sachac committed Jul 11, 2024
1 parent 997b1fa commit d5e4888
Show file tree
Hide file tree
Showing 10 changed files with 412 additions and 29 deletions.
5 changes: 4 additions & 1 deletion AUTHORS.org
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
Expand All @@ -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
9 changes: 9 additions & 0 deletions NEWS.org
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
#+OPTIONS: toc:nil

* subed news
** Version 1.2.14 - 2024-07-05 - Sacha Chua

- Bugfix: subed-waveform should now handle the
case where the stop time +
subed-waveform-preview-msecs-after might extend
past the end of the file.

Thanks to rodrigomorales1 and rnduser for the bug reports and pull requests!

** Version 1.2.13 - 2024-07-05 - Sacha Chua

- Bugfix: Fix the requires in subed-waveform to load subed-common.
Expand Down
197 changes: 174 additions & 23 deletions subed/subed-waveform.el
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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion subed/subed-waveform.el.license
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
2 changes: 1 addition & 1 deletion subed/subed.el
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
Expand Down
2 changes: 1 addition & 1 deletion tests/test-subed-mpv.el
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
;; -*- eval: (buttercup-minor-mode) -*-
;; -*- lexical-binding: t; eval: (buttercup-minor-mode) -*-

(load-file "./tests/undercover-init.el")
(require 'subed-mpv)
Expand Down
2 changes: 1 addition & 1 deletion tests/test-subed-srt.el
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
;; -*- eval: (buttercup-minor-mode) -*-
;; -*- lexical-binding: t; eval: (buttercup-minor-mode) -*-

(load-file "./tests/undercover-init.el")
(require 'subed-srt)
Expand Down
2 changes: 1 addition & 1 deletion tests/test-subed-tsv.el
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
;; -*- eval: (buttercup-minor-mode) -*-
;; -*- lexical-binding: t; eval: (buttercup-minor-mode) -*-

(add-to-list 'load-path "./subed")
(require 'subed)
Expand Down
Loading

0 comments on commit d5e4888

Please sign in to comment.