Skip to content

Instantly share code, notes, and snippets.

@FichteFoll
Created October 1, 2015 23:42
Show Gist options
  • Save FichteFoll/08b9d98699438b768552 to your computer and use it in GitHub Desktop.
Save FichteFoll/08b9d98699438b768552 to your computer and use it in GitHub Desktop.
Karaoke effect using Automation 4 for Ore no Imouto TR 15 (Ending)
-- This script was written by FichteFoll for http://akatsuki-subs.net
-- You can see this FX in Ore no Imouto TR [True Route] 15 (Ending)
-- MIT license, (c) FichteFoll
--[[
Remarks:
I don't really know what I should mention here.
Overall work time on this file (including searching for inspiration): 7 hours
2014-12-24
]]
require("karaskel")
require("op_overloads") -- https://gist.github.com/b6815727db496ceed1ae
-- use the never-changing parts for my Automation FXes from this include file
require("FX_utils")
script_name = "Oreimo TR15 FX"
script_description = "Nothing yet"
script_author = "FichteFoll"
script_version = 0.1
math.randomseed(1337)
--------------------------------------------------------------------------------
-- variables/constants
local tmp = {
}
local _line = {
fadein = {
start = -100,
dur = 250,
mid = 150,
diff = 40,
accel = 0.5,
scale_y = 20,
bord_scale = 2/3.0
},
fadeout = { -- subs only
dur = 500
},
shad = 1,
blur = 2
}
local _syl = {
hl = {
bord = 5,
blur = 3,
color = "&HFFFFFF&",
alpha = "&H10&",
accel = 0.25,
scale = 100,
startfade = 0.4,
fadeaccel = 2,
extend = 150
},
hl2 = {
move = 50,
diff = 70
},
fadeout = {
dur = 200,
accel = 0.2
}
}
--------------------------------------------------------------------------------
local log
function init(subs, meta, styles)
log = aegisub.log
end
function do_fx(subs, meta, styles, line, baseline)
if not line.style:startswith("ed15_") then
return -- not processed
end
-- use to differentiate between karaoke styles and the actual subtitles
local romaji = line.style:startswith("ed15_rom")
local kanji = line.style:startswith("ed15_kan")
local trans = line.style:startswith("ed15_ger")
-- pre-process1
parse_style_colors(line, styles[line.style])
-- parse_line_colors(line)
-- pre-process2
if trans then
-- create syllables for each word in translation (but not in comments)
line.text = "{\\k1}" .. line.text:gsub("%s*{.-}%s*", ''):gsub(" ", " {\\k1}")
end
karaskel.preproc_line(subs, meta, styles, line)
parse_syls(line)
-- general line parts
line._pos = "\\an5\\pos(%d,%d)" % {line.center, line.middle} -- will be overwritten, likely
line._pre = "\\shad%s\\blur%s" % {_line.shad, _line.blur}
line._color = ""
local slide_length = not romaji
and line.width + line.style.margin_l + 20
or line.width + line.style.margin_v + 20
-- differ between romaji and kanji (x and y); [true] is for romaji
local dimensions = {[true] = "y", [false] = "x"}
local slide = {[true] = slide_length, [false] = 0}
if trans then
-- log(repr(line._noblank) .. "\n")
end
for si, syl in ipairs(line._noblank) do
for ci, char in ipairs(get_charsyls(syl)) do
-- no need to handle whitespaces
if (syl.text_stripped == ' ' or syl.text_stripped == ' ') then
goto skipchar -- functions as `continue`
end
local l = table.copy(line)
-- fadein and hl1 are per syl for romaji & trans
if kanji or ci == 1 then
local middle = not kanji
and line.middle
or line.styleref.margin_v + char.left
local center = not kanji
and line.left + syl.center
or meta.res_x - line.styleref.margin_r - char.width/2.0
-- Fadein -----------------
local start_rel = (si - 1) * -_line.fadein.diff - _line.fadein.dur
l._pos = "\\an5\\move(%.2f,%.2f,%.2f,%.2f,%d,%d)" % {
center - slide[not kanji],
middle - slide[kanji],
center, middle,
0, _line.fadein.mid
}
l._fadein =
"\\fsc%%s%.2f\\%%sbord%.1f\\t(%d,%d,%.2f,\\fsc%%s%.2f\\%%sbord%.1f)" % {
_line.fadein.scale_y, line.styleref.outline * _line.fadein.bord_scale,
_line.fadein.mid, _line.fadein.dur, _line.fadein.accel,
line.styleref.scale_y, line.styleref.outline
}
l._fadein = l._fadein % {
dimensions[not kanji], dimensions[not kanji],
dimensions[not kanji], dimensions[not kanji]
}
l.start_time = line.start_time + start_rel
-- handle entire trans line here
if trans then
l._pre = l._pre .. "\\fad(0,300)"
else
l.end_time = line.start_time + syl.start_time
end
-- construct line text
l.text = string.concat(
'{', l._pos, l._pre, l._fadein, '}',
(kanji and char or syl).text_stripped
)
subs.append(l)
if trans then
goto skipchar -- `continue`
end
-- Highlight ---------------
l._pos = "\\an5\\pos(%.2f,%.2f)" % {
center, middle
}
l._pre = "\\shad0\\blur%s" % {_syl.hl.blur}
l._hl = "\\bord%s\\1a&HFF&\\3c%s\\3a%s" % {
_syl.hl.bord, _syl.hl.color, _syl.hl.alpha
}
..
"\\t(%.2f,\\fscx%.2f\\fscy%.2f)\\t(%d,%d,%.2f,\\3a&HFF&)" % {
_syl.hl.accel, _syl.hl.scale, _syl.hl.scale,
syl.duration * _syl.hl.startfade, syl.duration + _syl.hl.extend,
_syl.hl.fadeaccel
}
l.start_time = l.end_time
l.end_time = l.start_time + syl.duration + _syl.hl.extend
-- construct line text
l.text = string.concat(
'{', l._pos, l._pre, l._hl, '}', (kanji and char or syl).text_stripped
)
subs.append(l)
end
-- per char this time
local middle = romaji
and line.middle
or line.styleref.margin_v + char.left
local center = romaji
and line.left + char.center
or meta.res_x - line.styleref.margin_r - char.width/2.0
-- Highlight2 -------------- (per char also for romaji)
l = table.copy(line)
local move_speed = 1.0 * _syl.hl2.move / syl.duration
local hl_end = (ci-1) * _syl.hl2.diff + syl.duration
local slide_hl = {[true] = hl_end * move_speed, [false] = 0}
l._pos = "\\an5\\move(%.2f,%.2f,%.2f,%.2f)" % {
center, middle,
center - slide_hl[romaji],
middle - slide_hl[not romaji]
}
l._hl = ""
l.start_time = line.start_time + syl.start_time
l.end_time = l.start_time + hl_end
-- construct line text
l.text = string.concat(
'{', l._pos, l._pre, l._hl, '}', char.text_stripped
)
subs.append(l)
-- Fadeout -------------- (per char also for romaji)
local slide_out = {[true] = _syl.fadeout.dur * move_speed,
[false] = romaji and -line.height/2 or meta.res_x + char.width/2}
local out_pos = {[true] = romaji
and center - slide_hl[romaji] - slide_out[romaji]
or middle - slide_hl[not romaji] - slide_out[not romaji],
[false] = slide_out[false]}
l._pos = "\\an5\\move(%.2f,%.2f,%.2f,%.2f)" % {
center - slide_hl[romaji],
middle - slide_hl[not romaji],
out_pos[romaji],
out_pos[not romaji]
}
l._fadeout = "\\t(%.2f,\\fr%s90)" % {_syl.fadeout.accel, dimensions[not romaji]}
l.start_time = l.end_time
l.end_time = l.start_time + _syl.fadeout.dur
-- construct line text
l.text = string.concat(
'{', l._pos, l._pre, l._fadeout, '}', char.text_stripped
)
subs.append(l)
end
::skipchar:: -- goto anchor
end
return 1
end
--[[
Outline:
- fadein from left side
- slightly move left on hl
- fadeout upwards, by syl and char
Fadein:
Effect:
- slide in from left, very thin vertically
- expand to full size once at target postion
- right to left?
- iterative
- somewhat short
Timeline:
[---step1----| ]
[ |-step2-]
step1
- \move (left to normal)
- \fscy < 20, or \fry
- \bord < normal
step2
- \fscy -> 100, or \fry -> 0
- \bord -> normal
Fadeout:
- move upwards after hl, per char
- carry over highlight movement
Highlight:
Effect:
- Move to left
- Also display shadow-ish border behind original position
Timeline:
[---------step1--------]
[---------step2--------]
[ |-----step3----]
step1
- movement
step2 (accelerated)
- "grow" from shadow (currently none)
step3 (accelerated)
- fade
Subtitles:
-
]]
register()
--[[
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: 2014-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
-- Create a list of charsyls used in char templates
function get_charsyls(syl)
local charsyls = {}
local charsyl = table.copy(syl)
local left = syl.left
for c in unicode.chars(syl.text_stripped) do
charsyl.text = c
charsyl.text_stripped = c
charsyl.text_spacestripped = c
charsyl.prespace, charsyl.postspace = "", "" -- for whatever anyone might use these for
charsyl.width = aegisub.text_extents(syl.style, c)
charsyl.left = left
charsyl.center = left + charsyl.width/2
charsyl.right = left + charsyl.width
charsyl.prespacewidth, charsyl.postspacewidth = 0, 0 -- whatever...
left = left + charsyl.width
table.insert(charsyls, table.copy(charsyl))
end
return charsyls
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
--[[
These are small operator overloads for general use in Lua, mainly to my likings.
Written by FichteFoll; thanks to Sleepy_Coder
2012-12-24
]]
local STR, NUM, BOOL, NIL = {}, {}, {}, {}
STR = getmetatable('')
-- ("hello")[2] -> e
STR.__index
= function (self, key)
if type(key) == 'number' then
if key < 1 or key > self:len() then
error(("Attempt to get index %d which is not in the range of the string's length"):format(key), 2)
end
return self:sub(key, key)
end
return string[key]
end
-- str = "heeeello"; str[3] = "is" -> "heisello" || DOES NOT WORK!
STR.__newindex =
function (self, key, value)
value = tostring(value)
if type(key) == 'number' and type(value) == 'string' then
if key < 1 or key > self:len() then
error(("Attempt to set index %d which is not in the range of the string's length"):format(key), 2)
end
-- seems like strings are not referenced ...
self = self:sub(1, key-1) .. value .. self:sub(key+value:len(), -1)
-- print(("new value: %s; key: %d"):format(self, key))
return self
end
end
-- string * num -> string.rep
STR.__mul =
function (op1, op2)
return type(op2) == 'number' and op1:rep(op2) or error("Invalid type for arithmetic on string", 2)
end
-- string % table -> string.format
STR.__mod =
function (op1, op2)
if type(op2) == 'table' then
if #op2 > 0 then
-- make `nil` to string
for k,v in pairs(op2) do
if v == nil then op2[k] = "nil"; end
end
return op1:format(unpack(op2)) -- sadly I can not forward errors happening here
else
error("Format table is empty", 2)
end
else
return op1:format(op2 == nil and "nil" or op2)
end
end
-- #string -> count chars
STR.__len =
function (self)
return self:len()
end
-- e.g. num = 1234.5; num:floor()
NUM.__index = math
-- rarely useful
BOOL.__index = BOOL
-- a wrapper for boolean tests
BOOL.b2n =
function (bool)
return type(bool) ~= 'boolean' and bool or (bool and 1 or 0)
end
-- various arithmetics on booleans by converting `false` to `0` and `true` to `1`.
-- `nil` will be converted to `0` as well, btw.
BOOL.__add =
function (op1, op2)
-- op2.b2n() won't work because it is possible that one of these ops is not a boolean
return BOOL.b2n(op1) + BOOL.b2n(op2)
end
BOOL.__sub =
function (op1, op2)
return BOOL.b2n(op1) - BOOL.b2n(op2)
end
BOOL.__mul =
function (op1, op2)
return BOOL.b2n(op1) * BOOL.b2n(op2)
end
BOOL.__div =
function (op1, op2)
return BOOL.b2n(op1) / BOOL.b2n(op2)
end
BOOL.__pow =
function (op1, op2)
return BOOL.b2n(op1) ^ BOOL.b2n(op2)
end
BOOL.__unm =
function (self)
return not self
end
-- copy BOOL's functions over to NIL and remove a few values
for key, val in pairs(BOOL) do
NIL[key] = val
end
NIL.b2n = nil
NIL.__unm = nil
-- nil[3] -> nil (no error) - is this behaviour useful?
NIL.__index = NIL
-- Apparently, Aegisub does not provide the debug module ...
if debug then
debug.setmetatable( 0, NUM )
debug.setmetatable(true, BOOL)
debug.setmetatable( nil, NIL )
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment