2024-09-02 Rounding all timestamps in an srt file

This is another time I’m going to revisit subtitling in Emacs. This time I’m going to fix yet another minor annoyance. The srt files contain timestamps with millisecond resolution, which doesn’t make sense at all – when I edit subtitles for a video with 50fps, I don’t really need such precision. Instead, it makes sense for every timestamp to be rounded to the nearest 20 millisecond.

I decided to write a simple piece of Elisp to do the rounding for me. A quick scan through Subed source code revealed a few functions which could be helpful with that: subed-subtitle-msecs-start, subed-set-subtitle-time-start (and similar functions for -stop), and subed-forward-subtitle-id. While browsing the source code I also found the subed-for-each-subtitle macro, which is ideally suited to what I want to do.

So, here is my first attempt to do this.

(defun subed-round-current-timestamps (resolution)
  "Round the timestamps of the current subtitle to RESOLUTION msecs."
  (subed-set-subtitle-time-start
   (* resolution
      (round (subed-subtitle-msecs-start)
             resolution)))
  (subed-set-subtitle-time-stop
   (* resolution
      (round (subed-subtitle-msecs-stop)
             resolution))))

;; Note: first attempt, doesn’t work 100% correctly!
(defun subed-round-timestamps (&optional resolution)
  "Round every timestamp to RESOLUTION milliseconds."
  (interactive "*P")
  (setq resolution (or resolution 20))
  (subed-for-each-subtitle
    (point-min)
    (point-max)
    nil
    (subed-round-current-timestamps resolution)))

(Well, I cheated a bit – my real first attempt was botched because I didn’t read the docstring of round, just looked at its signature, and did not multiply the result by 20.)

As usual, it turns out that the first attempt does not work completely well. In this case, the bug was a bit subtle. Subed mode maintains a minimal temporal distance between the stop time of one subtitle and the start time of the next one, which is 100 milliseconds by default, and that setting affects functions which change the timestamps. Of course, the remedy is simple – it’s enough to turn that customization off for the time of performing the change. Because of Emacs’ dynamic scoping used for options, this is as easy as putting a let around my code.

(defun subed-round-timestamps (&optional resolution)
  "Round every timestamp to RESOLUTION milliseconds."
  (interactive "*P")
  (setq resolution (or resolution 20))
  (let ((subed-enforce-time-boundaries nil))
    (subed-for-each-subtitle
      (point-min)
      (point-max)
      nil
      (subed-round-current-timestamps resolution))))

And that’s pretty much it – it just works now! At first, I thought that I’d have to remember to turn the Subed waveform minor mode off so that the waveforms don’t get updated suring the iteration, but for some reason it turned out to be unnecessary. This is because the Subed waveform mode uses subed-subtitle-motion-hook, which itself is run within post-command-hook – and that is only used when the command is run interactively. (Conversely, when you use C-M-f, that is, subed-shift-subtitle-forward, to move all subtitles by some amount, you need to disable waveform mode manually. Now that I think about it, I might submit a fix for that.)

Also, while checking (again) how to use the numeric prefix as the resolution but default to 20, I discovered the N interactive code. It reads a number from the minibuffer, but uses the numeric prefix argument instead if specified. This is something I needed in my command downloading a Jira task, only I coded that logic manually. I thought it’s a new development, but it turned out that it has been in Emacs for about three decades, possibly more! I definitely don’t want it here – the default of 20 is much more reasonable than asking the user every time – but it’s nice to be aware that every time you look into Emacs docs, you may find something you didn’t know about.

That’s it for today!

CategoryEnglish, CategoryBlog, CategoryEmacs