Created
December 24, 2013 14:48
-
-
Save FichteFoll/8114301 to your computer and use it in GitHub Desktop.
Oreimo ED14 [True Route] Karaoke-FX in Lua
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
-- 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() |
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
--[[ | |
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 |
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
--[[ | |
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