Skip to content

Instantly share code, notes, and snippets.

@FichteFoll
Created August 22, 2014 17:17
Show Gist options
  • Save FichteFoll/b7f8eb6015b5f33ab0ae to your computer and use it in GitHub Desktop.
Save FichteFoll/b7f8eb6015b5f33ab0ae to your computer and use it in GitHub Desktop.
FX_utils.lua for Aegisub Automation karaokes
--[[
A template script used for Automation 4 karaoke FXes with Aegisub
You will want to call `register()` at the end of your script or whenever at least `script_name` is defined.
Functions to be defined:
* init(subs, meta, styles) [optional]
Called after all possibly existing previous fx lines have been deleted and the karaoke lines restored.
Do whatever you like in here. Won't be called if `init` is nil.
@subs
The subs table passed by aegisub.
@meta, @styles
Results of karaskel.collect_head(subs, false).
* do_fx(subs, meta, styles, line, baseline)
Called when is_parseble(line) returns `true`, i.e. when you should parse that line.
The parsed line will be commented and its effect set to "karaoke" iff this function returns a value evaluating
to `true`.
@subs
Same as for init().
@meta, @styles
Same as for init().
@line
This is the line source you will be using. You can do anything with it, it's in fact a table.copy of
@baseline with its effect set to "fx" and layer to 2.
Has a field named "_i" which defines the line's index in `subs` and a field named "_li" which counts up
every time do_fx is called.
@baseline
The table representing the line.
This is a REFERENCE, you need to table.copy() this before if you want to insert a new copy.
The line itself should not be modified at all, e.g. when you plan to run this macro a few times.
Also defines "_i" and "_li" (see @line).
Variables to be defined (before calling `register()`):
* script_name
Includes a variety of utility functions.
By FichteFoll, last modified: 2013-12-23
]]
require("karaskel")
-- ############################# handler funcs #################################
function macro_script_handler(subs)
aegisub.progress.title("Apply "..script_name)
script_handler(subs)
aegisub.set_undo_point('"Apply '..script_name..'"')
end
function script_handler(subs)
aegisub.progress.task("Getting header data...")
local meta, styles = karaskel.collect_head(subs, false)
-- undo the fx before parsing the karaoke data
aegisub.progress.task("Removing old karaoke lines...")
undo_fx(subs)
aegisub.progress.task("Applying effect...")
local i, maxi = 1, #subs
local li = 0
if init then
init(subs, meta, styles)
end
while i <= maxi do
local l = subs[i]
if aegisub.progress.is_cancelled() then
if aegisub.cancel then aegisub.cancel() end
return
end
aegisub.progress.task(string.format("Applying effect (%d/%d)...", i, maxi))
aegisub.progress.set((i-1) / maxi * 100)
if line_is_parseable(l) then
-- karaskel.preproc_line(subs, meta, styles, l)
li = li + 1
l._li = li
l._i = i
-- prepare the to-be-copyied line
local line = table.copy(l)
line.effect = "fx"
line.layer = 2
if do_fx(subs, meta, styles, line, l) then
l.comment = true
l.effect = "karaoke"
end
subs[i] = l
end
i = i + 1
end
aegisub.progress.task("Finished!")
aegisub.progress.set(100)
end
function undo_macro_script_handler(subs, ...)
aegisub.progress.title("Undoing "..script_name)
undo_fx(subs)
aegisub.set_undo_point("\"Undo "..script_name.."\"")
end
function undo_fx(subs)
aegisub.progress.task("Unapplying effect...")
local i, maxi = 1, #subs
local ai, maxai = i, maxi
while i <= maxi do
if aegisub.progress.is_cancelled() then
if aegisub.cancel then aegisub.cancel() end
return
end
aegisub.progress.task(string.format("Unapplying effect (%d/%d)...", ai, maxai))
aegisub.progress.set((ai-1) / maxai * 100)
local l = subs[i]
if (l.class == "dialogue" and not l.comment and l.effect == "fx") then
subs.delete(i)
maxi = maxi - 1
else
if (l.class == "dialogue" and l.comment and l.effect == "karaoke") then
l.comment = false
l.effect = ''
subs[i] = l
end
i = i + 1
end
end
end
-- ############################### parsing funcs ###############################
function parse_syls(line)
-- parse the line's syllables and add them to the `line._noblank` table
-- requires you to call karaskel.preproc_line first
line._noblank = {n = 0}
for i = 1, line.kara.n do
local syl = line.kara[i]
if (syl.duration > 0 and syl.text_stripped ~= ''
and syl.text_stripped ~= ' ' and syl.text_stripped ~= ' ') then
line._noblank.n = line._noblank.n + 1
line._noblank[line._noblank.n] = syl
syl.i = line._noblank.n
syl._blank = false
else
syl._blank = true
end
end
end
function parse_style_colors(line, style)
line._c = {}
line._a = {}
for i, c, a in colors_from_style(style) do
line._c[i] = c
line._a[i] = a
end
end
-- Parse the color overrides on the line (assuming they are the only blocks before the {\k}-block)
-- and add them to line._colors
-- Lines should look like this:
-- {\c&H8D566C&\2c&HC982AE&\3c&H1A0110&}{\t(720,720,,\c&HFFFFFF&\2c&HA99E4F&\3c&H161501&)}
-- {\t(1900,1900,\2c&HBCA2C6&\c&H605064&\3c&H0B0006&)}{\k24}sho{\k24}se{\k16}n ...
function parse_line_colors(line)
line._colors = {_all = ""}
local colors_str, text = line.text:match("^(.-)({\\k.+)$")
if not colors_str then
-- we are probably in a "sub" block, just consume all the override blocks
-- wat?
colors_str = ""
text = line.text:gsub("({.-})", function (override)
colors_str = colors_str .. override
return ""
end)
end
if not colors_str then log("No match on line: %s\n", line.text); end
if colors_str and #colors_str > 0 then
-- collect color blocks
for str in colors_str:gmatch("{(.-)}") do
-- always assuming there is only one \t override tag each
local start, stop, colors = str:match("\\t%((%d+),(%d+),(%S-)%)")
-- log("start: %s, stop: %s, colors: %s\n" % {start or "nil", stop or "nil", colors or "nil"})
if not colors then
colors = str:match("\\t%((%S-)%)")
end
local block = {
text = str,
start = tonumber(start) or 0,
stop = tonumber(stop) or (colors and line.duration or 0),
colors = colors or str
}
if not colors then -- supposed to happen only once
line._colors._base = block
else
table.insert(line._colors, block)
end
line._colors._all = line._colors._all .. str
end
-- log(repr(line._colors) .. "\n")
end
end
-- ############################### helping funcs ###############################
-- Returns the selected color `i` (or the first one) at `timestamp` given a few override codes.
-- Does not check for validity anywhere; do not abuse. Returns nil if color `i` was not found.
function color_at(col, timestamp, i)
if not i then i = "[1-4]"; end -- match any color number in the pattern
if i == 1 then i = "1?"; end -- 1 can be omitted
function _ret(col)
-- used to select the specified color "i"; nil if not found
return col.colors:match("\\"..i.."c(&H%w+&)") or nil
end
local active, last
-- iterate over override blocks for the currently active transition, consider only the last
local start, stop
for _, block in ipairs(col) do
if (timestamp > block.start and timestamp < block.stop and _ret(block)) then
active = block
end
end
-- ... for the lastly active transition
for _, block in ipairs(col) do
if (timestamp > block.stop and _ret(block)) then
last = block
end
end
if not last then last = col._base; end
if not active then
return _ret(last) -- just return the last color
end
-- select the specified color
local start, stop = _ret(last), _ret(active)
-- either no color or no "new" color found
if not stop then
-- log("stcol: %s\n", last.colors)
-- log("start: %s\n", start)
return start or nil
end
-- interpolate the color
local pct = (timestamp - active.start) * 1.0 / (active.stop - active.start)
return interpolate_color(pct, start, stop)
end
-- Searches for \t-tag times and shifts them by `by`
function shift_ttags(str, by)
return str:gsub("\\t%((%d+),(%d+)", function (start, stop)
return "\\t(%d,%d" % {tostring(tonumber(start) - by),
tostring(tonumber(stop) - by)}
end)
end
function repr(val)
if type(val) == "table" then
local str = "{" --"#%d:{" % #val
for k, v in pairs(val) do
str = str .. ("%s = %s, "):format(k, repr(v))
end
return str:sub(1, -3) .. "}" -- trim last ", "
elseif type(val) == "string" then
return '"%s"' % val
else
return tostring(val)
end
end
function colors_from_style(style)
local i = 0
function enum_colors()
i = i + 1
if (i > 4) then
return nil
end
-- i, color, alpha
return i, color_from_style(style["color"..tostring(i)]), alpha_from_style(style["color"..tostring(i)])
end
return enum_colors, nil, nil
end
function strip_comments(str)
return str:gsub("{.-}", "")
end
function xor(...)
-- return the first parameter which evaluates to `true`
args = {...}
for v in args do
if v then return v; end
end
return args[#args] -- return the last element if none is true
end
function _if(test, a, b)
return test and a or b
end
function line_is_parseable(l)
return (l.class == "dialogue" and not l.comment)
or (l.class == "dialogue" and l.comment and l.effect == "karaoke")
end
function round(num, idp)
local mult = 10^(idp or 0)
if num >= 0 then return math.floor(num * mult + 0.5) / mult
else return math.ceil(num * mult - 0.5) / mult end
end
function randomfloat(min, max)
return math.random() * (max - min) + min
end
function string.concat(...)
ret = ""
for _, str in pairs{...} do
-- there shouldn't be any nils here
ret = ret..tostring(str)
end
return ret
end
function string.startswith(self, piece)
return string.sub(self, 1, string.len(piece)) == piece
end
function string.endswith(self, piece)
return string.sub(self, -string.len(piece)) == piece
end
-- ############################### validation funcs ############################
function macro_validation(subs)
for i = 1, #subs do
local l = subs[i]
if line_is_parseable(l) then
return true
end
end
return false, "No parseable line found"
end
function undo_macro_validation(subs)
for i = 1, #subs do
local l = subs[i]
if (l.class == "dialogue" and (
(not l.comment and l.effect == "fx") or
( l.comment and l.effect == "karaoke") )) then
return true
end
end
return false, "No karaoke line found"
end
-- ############################## registering ##################################
function register()
aegisub.register_macro("Apply "..script_name,
"Processing script as templater",
macro_script_handler,
macro_validation)
-- should not be needed (for my setup at least, I have a separate macro which does this)
-- aegisub.register_macro("Undo "..script_name, "Removing templated lines", undo_macro_script_handler, undo_macro_validation)
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment