All these generate files with resolution 960x540. The intent is to have something
to watch in a VR headset while playing piano scales, as an exercise in weaning myself
off the habit of watching my hands, and consequently compromising posture.
Set MERGE=y to use ffmpeg, else it will simply generate the subtitles files.
The purpose of the timer is so that I can e.g. practise 5 mins LH, 5 mins RH,
5 mins hands together, etc.
16:9 version
#!/usr/bin/env python
'''
We can use \\fs to change font size, and \\c to change colour.
So we can have white 60px text most of the time, and then change each second
approaching a 5m:
'''
import sys
import os
import math
merge = os.getenv("MERGE","n").lower() in ["y","yes","true","1"]
from subprocess import run, DEVNULL
import json
def getvinfo(fn):
fn = str(fn)
m = run(["ffprobe","-print_format","json","-show_streams","-show_format",fn],capture_output=True,stdin=DEVNULL)
if m.returncode != 0:
print(f"ffprobe on {fn} returned {m.returncode}")
return None
return json.loads(m.stdout.decode())
def render(ifn,sfn,ofn,vwidth,vheight,opts=[]):
cmd = ["ffmpeg","-i",ifn,"-vf",f"scale={vwidth}:{vheight},subtitles={sfn}","-aspect",f"{vwidth}:{vheight}"]+opts+[ofn]
m = run(cmd,stdin=DEVNULL)
rc = m.returncode
if rc != 0:
print(f"ffmpeg returned",rc)
return rc
def main():
args = sys.argv[1:]
if len(args) == 0:
args = ["gm.mp4"]
if len(args) == 0:
print(f"{sys.argv[0]} <fn> [<fn> ...]")
exit()
for fn in args:
proc(fn)
def proc(fn):
if not os.path.isfile(fn):
print(f"File {s} does not exist or is not a file.")
return
try:
info = getvinfo(fn)
except Exception as e:
print(f"Exception {e} ({type(e)}) getting video info for {fn}")
return
sfn = f"{fn}.timer.ass"
fmt = info['format']
sts = info['streams']
vids = None
for s in sts:
if s['codec_type'] == "video":
vids = s
break
else:
print(f"No video stream in {fn}")
return
dur = math.ceil(float(fmt['duration']))
out = makeout(dur,960,540)
out = "\n".join(out)
with open(sfn,"wt") as f:
print(out,file=f)
print("Written",sfn)
ofn = f"s_{fn}"
if merge:
render(fn,sfn,ofn,960,540,["-b:a","128k","-c:v","h264_nvenc","-b:v","1M"])
def makeout(dur,vwidth,vheight):
x = int(vwidth*1/3)
y = int(vheight*7/8)
fsb = int(vheight/7)
out = [f"""[Script Info]
PlayResY: {vheight}
PlayResX: {vwidth}
WrapStyle: 1
[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, Bold, BorderStyle, Outline, Shadow
Style: Timer, Optima, {fsb}, &H00FFFFFF, -1, 1, 2, 2
[Events]
Format: Layer, Start, End, Style, Text"""]
vh, vm, vs = tohms(dur)
leadin = 10
leadout = 5
period = 300
for t in range(dur):
tmod = t % period
#print(tmod,leadout,leadin,period-leadin)
c = ""
f = ""
h,m,s = tohms(t)
t0 = f"{h:02d}:{m:02d}:{s:02d}.00"
td = f"{m:02d}:{s:02d}"
if dur >= 3600:
td = f"{h:02d}:{td}"
h,m,s = tohms(t+1)
t1 = f"{h:02d}:{m:02d}:{s:02d}.00"
if tmod < leadout:
tt = 5 - tmod
fs = fsb + 10*tt
n = int((tmod/leadout)*255.0)
print(t,tmod,n)
f = f"{{\\fs{fs}}}"
c = f"{{\\c{n:02X}FF{n:02X}}}"
if tmod > (period-leadin):
tt = tmod - (period - leadin)
ts = 10 - tt
fs = fsb + 5*tt
n = int((ts/leadin)*255.0)
print(t,tt,tmod,n)
f = f"{{\\fs{fs}}}"
c = f"{{\\c{n:02X}FF{n:02X}}}"
event = f"Dialogue: 0,{t0},{t1},Timer, {{\\pos({x},{y})}}{f}{c}{td}"
out.append(event)
return out
def tohms(x):
h, x = divmod(x,3600)
m, s = divmod(x,60)
return h,m,s
if __name__ == "__main__":
main()
4:3 version
#!/usr/bin/env python
import sys
import os
import math
merge = os.getenv("MERGE","n").lower() in ["y","yes","true","1"]
from subprocess import run, DEVNULL
import json
def getvinfo(fn):
fn = str(fn)
m = run(["ffprobe","-print_format","json","-show_streams","-show_format",fn],capture_output=True,stdin=DEVNULL)
if m.returncode != 0:
print(f"ffprobe on {fn} returned {m.returncode}")
return None
return json.loads(m.stdout.decode())
def render(ifn,sfn,ofn,opts=[]):
cmd = ["ffmpeg","-i",ifn,"-vf",f"scale=676:540,pad=width=960:height=540:x=142:y=0,subtitles={sfn}","-aspect",f"960:540"]+opts+[ofn]
m = run(cmd,stdin=DEVNULL)
rc = m.returncode
if rc != 0:
print(f"ffmpeg returned",rc)
return rc
def main():
args = sys.argv[1:]
if len(args) == 0:
args = ["gm.mp4"]
if len(args) == 0:
print(f"{sys.argv[0]} <fn> [<fn> ...]")
exit()
for fn in args:
proc(fn)
def proc(fn):
if not os.path.isfile(fn):
print(f"File {s} does not exist or is not a file.")
return
try:
info = getvinfo(fn)
except Exception as e:
print(f"Exception {e} ({type(e)}) getting video info for {fn}")
return
sfn = f"{fn}.timer.ass"
fmt = info['format']
sts = info['streams']
vids = None
for s in sts:
if s['codec_type'] == "video":
vids = s
break
else:
print(f"No video stream in {fn}")
return
dur = math.ceil(float(fmt['duration']))
out = makeout(dur,960,540)
out = "\n".join(out)
with open(sfn,"wt") as f:
print(out,file=f)
print("Written",sfn)
ofn = f"s_{fn}"
#render(fn,sfn,ofn,960:540,["-b:a","128k","-c:v","
if merge:
render(fn,sfn,ofn,["-b:a","128k","-c:v","h264_nvenc","-b:v","1M","-t","300"])
def makeout(dur,vwidth,vheight):
x = int(vwidth*1/3)
y = int(vheight*7/8)
fsb = int(vheight/7)
out = [f"""[Script Info]
PlayResY: {vheight}
PlayResX: {vwidth}
WrapStyle: 1
[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, Bold, BorderStyle, Outline, Shadow
Style: Timer, Optima, {fsb}, &H00FFFFFF, -1, 1, 2, 2
[Events]
Format: Layer, Start, End, Style, Text"""]
vh, vm, vs = tohms(dur)
leadin = 10
leadout = 5
period = 300
for t in range(dur):
tmod = t % period
#print(tmod,leadout,leadin,period-leadin)
c = ""
f = ""
h,m,s = tohms(t)
t0 = f"{h:02d}:{m:02d}:{s:02d}.00"
td = f"{m:02d}:{s:02d}"
if dur >= 3600:
td = f"{h:02d}:{td}"
h,m,s = tohms(t+1)
t1 = f"{h:02d}:{m:02d}:{s:02d}.00"
if tmod < leadout:
tt = 5 - tmod
fs = fsb + 10*tt
n = int((tmod/leadout)*255.0)
print(t,tmod,n)
f = f"{{\\fs{fs}}}"
c = f"{{\\c{n:02X}FF{n:02X}}}"
if tmod > (period-leadin):
tt = tmod - (period - leadin)
ts = 10 - tt
fs = fsb + 5*tt
n = int((ts/leadin)*255.0)
print(t,tt,tmod,n)
f = f"{{\\fs{fs}}}"
c = f"{{\\c{n:02X}FF{n:02X}}}"
event = f"Dialogue: 0,{t0},{t1},Timer, {{\\pos({x},{y})}}{f}{c}{td}"
out.append(event)
return out
def tohms(x):
h, x = divmod(x,3600)
m, s = divmod(x,60)
return h,m,s
if __name__ == "__main__":
main()
Example Subtitles Files
[Script Info]
PlayResY: 540
PlayResX: 960
WrapStyle: 1
[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, Bold, BorderStyle, Outline, Shadow
Style: Timer, Optima, 77, &H00FFFFFF, -1, 1, 2, 2
[Events]
Format: Layer, Start, End, Style, Text
Dialogue: 0,00:00:00.00,00:00:01.00,Timer, {\pos(320,472)}{\fs127}{\c00FF00}00:00
Dialogue: 0,00:00:01.00,00:00:02.00,Timer, {\pos(320,472)}{\fs117}{\c33FF33}00:01
Dialogue: 0,00:00:02.00,00:00:03.00,Timer, {\pos(320,472)}{\fs107}{\c66FF66}00:02
Dialogue: 0,00:00:03.00,00:00:04.00,Timer, {\pos(320,472)}{\fs97}{\c99FF99}00:03
Dialogue: 0,00:00:04.00,00:00:05.00,Timer, {\pos(320,472)}{\fs87}{\cCCFFCC}00:04
Dialogue: 0,00:00:05.00,00:00:06.00,Timer, {\pos(320,472)}00:05
Dialogue: 0,00:00:06.00,00:00:07.00,Timer, {\pos(320,472)}00:06
Dialogue: 0,00:00:07.00,00:00:08.00,Timer, {\pos(320,472)}00:07