Skip to content

Instantly share code, notes, and snippets.

@FichteFoll
Created December 24, 2013 14:48
Show Gist options
  • Save FichteFoll/8114301 to your computer and use it in GitHub Desktop.
Save FichteFoll/8114301 to your computer and use it in GitHub Desktop.
Oreimo ED14 [True Route] Karaoke-FX in Lua
-- This script is made by FichteFoll for Akatsuki-Subs.net
-- You can see this FX in Ore no Imouto TR [True Route] 14 (Ending)
--[[
Remarks:
I would've used some operation overloading inspired from Python ((probably) up-to-date version here:
https://gist.github.com/b6815727db496ceed1ae) but Aegisub does not provide the debug module which is required to
set a types metatable that does not yet have one (everything except string).
2013-12-24
]]
require("karaskel")
require("op_overloads")
-- use the never-changing parts for my Automation FXes from this include file
require("FX_utils")
script_name = "Oreimo TR14 FX"
script_description = "Nothing yet"
script_author = "FichteFoll"
script_version = 0.8
math.randomseed(1337)
--------------------------------------------------------------------------------
-- variables/constants
local tmp = {
}
local _line = {
fadein = {
accel = 0.5,
start = -500,
dur = 450
},
blur = 1.5,
-- subs only
fadeout = {
dur = 900
}
}
local _syl = {
hl = {
accel = 3,
grow = 3,
shrink = 0.4,
blur = 4,
sidebord = 8,
extend = 300
},
fadeout = {
accel = 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("ed14_") then
return -- not processed
end
-- pre-process1
parse_style_colors(line, styles[line.style])
parse_line_colors(line)
-- pre-process2
karaskel.preproc_line(subs, meta, styles, line)
parse_syls(line)
-- use to differentiate between karaoke styles and the actual subtitles
local kara = not line.style:startswith("ed14_ger")
-- general calc
line._pos = "\\an5\\pos(%d,%d)" % {line.center, line.middle} -- will be overwritten
line._pre = "\\blur%s" % _line.blur
line._color = ""
local outtmpl = "%s\\t(%d,%d,%.2f,\\blur%s\\%sbord%s\\fscx%s\\fscy%s)"
.."\\t(%d,%d,%.2f,\\alpha&HFF&)"
local function shiftcols(l)
if #line._colors._all > 0 then
l._color = shift_ttags(line._colors._all, l.start_time - line.start_time)
end
end
if kara then
for si, syl in ipairs(line._noblank) do
local l = table.copy(line)
-- more calc
syl._left = line.left + syl.left
syl._right = line.left + syl.right
syl._center = line.left + syl.center
l._pos = "\\an5\\pos(%d,%d)" % {syl._center, line.middle}
-- Fadein -----------------
local fadetable = {
{ syl._left, line.top, -90, 90 },
{ syl._right, line.top, -90, -90 },
{ syl._left, line.bottom, 90, 90 },
{ syl._right, line.bottom, 90, -90 }
}
l._fadein = "\\org(%s,%s)\\frx%d\\fry%d" % fadetable[math.random(1, 4)]
.. "\\t(0,%d,%.2f,\\frx0\\fry0)" % { -- transition
_line.fadein.dur, _line.fadein.accel
}
l.start_time = line.start_time + _line.fadein.start
l.end_time = line.start_time + syl.start_time
-- construct line
shiftcols(l)
l.text = string.concat(
'{', l._pos, l._pre, l._fadein, l._color, '}', syl.text_stripped
)
subs.append(l)
-- Highlight --------------
local function hladd(l)
shiftcols(l)
l.text = string.concat(
'{', l._pos, l._pre, l._hl, l._color, '}', syl.text_stripped
)
subs.append(l)
end
-- vertically
l._hl = outtmpl % {'',
0, syl.duration + _syl.hl.extend, _syl.hl.accel,
-- sizing
_syl.hl.blur, 'y', _syl.hl.sidebord,
line.styleref.scale_x * _syl.hl.shrink,
line.styleref.scale_y * _syl.hl.grow,
-- fadeout
syl.duration / 2, syl.duration + _syl.hl.extend, _syl.fadeout.accel
}
l.start_time = line.start_time + syl.start_time
l.end_time = line.start_time + syl.end_time + _syl.hl.extend
-- construct line
hladd(l)
-- horizontally
l.layer = line.layer - 1
l._hl = outtmpl % {"\\alpha&H50&",
0, syl.duration + _syl.hl.extend, 1.0 / _syl.hl.accel,
-- sizing -- No border for horizontal stuff because it would double in the first frames
_syl.hl.blur, '', 0,
line.styleref.scale_x * _syl.hl.grow,
line.styleref.scale_y * _syl.hl.shrink,
-- fadeout
0, syl.duration + _syl.hl.extend, _syl.fadeout.accel
}
-- construct line
hladd(l)
end
else
local l = table.copy(line)
l._pre = l._pre .. "\\q2" -- no wrap
-- subs
local function subsadd(l)
shiftcols(l)
l.text = string.concat(
'{', l._pos, l._pre, l._hl, l._color, '}', line.text_stripped
)
subs.append(l)
end
-- Fadein ---------------------
l._fadein = "\\fad(%d,0)" % _line.fadein.dur
l.start_time = line.start_time - _line.fadein.dur
-- Fadeout ---------------------
local len = _line.fadein.dur + line.duration
-- vertically
l._hl = l._fadein .. outtmpl % {'',
len - _line.fadeout.dur, len, _syl.hl.accel,
-- sizing
_syl.hl.blur, 'y', _syl.hl.sidebord,
line.styleref.scale_x * _syl.hl.shrink,
line.styleref.scale_y * _syl.hl.grow,
-- fadeout
len - _line.fadeout.dur / 2, len, _syl.fadeout.accel
}
-- l.end_time = line.end_time
-- construct line
subsadd(l)
-- horizontally
l.layer = line.layer - 1
l._hl = outtmpl % {"\\alpha&H50&",
0, _line.fadeout.dur, _syl.hl.accel,
-- sizing -- No border for horizontal stuff because it would double in the first frames
_syl.hl.blur, '', 0,
line.styleref.scale_x * _syl.hl.grow,
line.styleref.scale_y * _syl.hl.shrink,
-- fadeout
0, _line.fadeout.dur, _syl.fadeout.accel
}
l.start_time = line.end_time - _line.fadeout.dur
-- construct line
subsadd(l)
end
return 1
end
--[[
Outline:
- some fancy hl
- fadeout during hl
Fadein:
- all syls rotate in at once from random direction
- Timeline:
[----------step1--------]
step1 (accelerated)
- \fry, \frx -> 0
Fadeout:
- bound to highlight
Highlight:
- Effect:
Two layers of syl that grow horizontally or vertically and shrink in the other dir
One side is reverse-accelerated
- Timeline:
[---------step1---------]
[ |---step2----]
step1 (accelerated!)
- grow one side
- shrink the other
step1 (accelerated)
- fade
Subtitles:
- alpha fadein
- fadout like hl
]]
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: 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
--[[
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