Created
July 8, 2025 03:41
-
-
Save ion1/b897a8f9a37b78efa49dd5a38093df21 to your computer and use it in GitHub Desktop.
subtitle-speed mpv script
This file contains hidden or 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
-- Adjust the subtitle speed while keeping the subtitle track synchronized to | |
-- the video at the moment when the subtitle delay was adjusted the last time | |
-- (or when the subtitle speed was adjusted by something other than this | |
-- script). | |
local mp = require("mp") | |
local msg = require("mp.msg") | |
local options = { | |
key_cycle_speed_prev = "Alt+z", | |
key_cycle_speed_next = "Alt+x", | |
} | |
require("mp.options").read_options(options) | |
---@class PossibleSpeed | |
---@field label string | |
---@field value number | |
---@type PossibleSpeed[] | |
local possible_speeds = { | |
{ label = "100 %", value = 1 }, | |
{ label = "24/23.976", value = 24 / (24000 / 1001) }, | |
{ label = "23.976/24", value = (24000 / 1001) / 24 }, | |
{ label = "25/23.976", value = 25 / (24000 / 1001) }, | |
{ label = "23.976/25", value = (24000 / 1001) / 25 }, | |
{ label = "25/24", value = 25 / 24 }, | |
{ label = "24/25", value = 24 / 25 }, | |
} | |
table.sort(possible_speeds, function(left, right) | |
return right.value < left.value | |
end) | |
-- The 100 % entry in the middle. | |
local default_speed_ix = math.floor((#possible_speeds + 1) / 2) | |
---@class State | |
---@field anchor_video_time number | |
---@field anchor_sub_time number | |
---@field speed_ix number | |
-- Set permanently if we changed any properties. For resetting them upon another | |
-- file being loaded. | |
---@field manipulated_properties boolean | |
-- Set temporarily to indicate that the property observer should not react. | |
---@field changed_properties table<string, boolean> | |
---@type State | |
local state = { | |
anchor_video_time = 0, | |
anchor_sub_time = 0, | |
speed_ix = default_speed_ix, | |
manipulated_properties = false, | |
changed_properties = {}, | |
} | |
---@alias PropertyName "sub-speed" | "sub-delay" | |
local script_name = mp.get_script_name() | |
---@param message string | |
---@param osd boolean | nil | |
local function info(message, osd) | |
if osd == nil then | |
osd = true | |
end | |
local full_message = "[" .. script_name .. "] " .. message | |
msg.info(full_message) | |
if osd then | |
mp.osd_message(full_message, 3) | |
end | |
end | |
---@param name PropertyName | |
---@param value number | |
local function set_property(name, value) | |
mp.set_property_number(name, value) | |
state.changed_properties[name] = true | |
end | |
local function reset() | |
state.anchor_video_time = 0 | |
state.anchor_sub_time = 0 | |
state.speed_ix = default_speed_ix | |
if state.manipulated_properties then | |
set_property("sub-speed", 1) | |
set_property("sub-delay", 0) | |
state.manipulated_properties = false | |
end | |
end | |
local function mark_anchor() | |
local video_time = mp.get_property_number("time-pos") | |
local sub_speed = mp.get_property_number("sub-speed") | |
local sub_delay = mp.get_property_number("sub-delay") | |
if video_time == nil or sub_speed == nil or sub_delay == nil then | |
return | |
end | |
info("Marking anchor", false) | |
local sub_time = (video_time - sub_delay) / sub_speed | |
state.anchor_video_time = video_time | |
state.anchor_sub_time = sub_time | |
end | |
---@param name PropertyName | |
---@param value number | |
local function property_changed(name, value) | |
if state.changed_properties[name] then | |
state.changed_properties[name] = nil | |
else | |
mark_anchor() | |
end | |
end | |
---@param n number | |
---@param scale number | |
---@return number | |
local function round_to(n, scale) | |
return math.floor(n * scale + 0.5) / scale | |
end | |
local function update() | |
local possible_speed = possible_speeds[state.speed_ix] | |
local sub_speed_label, sub_speed = possible_speed.label, possible_speed.value | |
local sub_delay_unrounded = state.anchor_video_time - state.anchor_sub_time * sub_speed | |
local sub_delay = round_to(sub_delay_unrounded, 10) | |
info(string.format("Setting speed: %s, delay: %d ms", sub_speed_label, 1000 * sub_delay)) | |
set_property("sub-speed", sub_speed) | |
set_property("sub-delay", sub_delay) | |
end | |
---@param speed_ix integer | |
---@return integer | |
local function clamp_speed_ix(speed_ix) | |
return math.min(math.max(speed_ix, 1), #possible_speeds) | |
end | |
local function cycle_speed_prev() | |
state.speed_ix = clamp_speed_ix(state.speed_ix - 1) | |
update() | |
end | |
local function cycle_speed_next() | |
state.speed_ix = clamp_speed_ix(state.speed_ix + 1) | |
update() | |
end | |
mp.add_key_binding(options.key_cycle_speed_prev, "subtitle-sync-cycle-speed-prev", cycle_speed_prev) | |
mp.add_key_binding(options.key_cycle_speed_next, "subtitle-sync-cycle-speed-next", cycle_speed_next) | |
mp.register_event("start-file", reset) | |
mp.observe_property("sub-speed", "number", property_changed) | |
mp.observe_property("sub-delay", "number", property_changed) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment