Skip to content

Instantly share code, notes, and snippets.

@ion1
Created July 8, 2025 03:41
Show Gist options
  • Save ion1/b897a8f9a37b78efa49dd5a38093df21 to your computer and use it in GitHub Desktop.
Save ion1/b897a8f9a37b78efa49dd5a38093df21 to your computer and use it in GitHub Desktop.
subtitle-speed mpv script
-- 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