The main purpose for this is to quickly produce a video for upload to youtube from
a .wav file and a backdrop image.
wavimg2mp4
This requires Pillow (python -m pip install Pillow)
Usage:
wavimg2mp4 <audio file> <image file> <output filename> [output-width output-height]
It takes three (or 5) arguments. The default resolution is 1920x1080.
It expands or contracts the image to fill the target resolution, cropping as necessary.
It then makes a 60 second loop of that image using ffmpeg, then forms a catlist file
consisting of as many file 'loop.mp4' lines as necessary that the end result is just longer
than the .wav file. Then it uses ffmpeg to merge these two, cropping the result to
the length of the .wav file.
#!/usr/bin/env python
from subprocess import run, PIPE, DEVNULL
from PIL import Image
from glob import glob
import sys
import json
import math
def main():
args = sys.argv[1:]
try:
wavfn, imgfn, ofn, *xs = args
if len(xs) == 2:
output_w, output_h = map(int,xs)
else:
output_w, output_h = 1920, 1080
except Exception:
print(f"{sys.argv[0]} wavfn imgfn ofn [output_w output_h]")
exit(1)
imgfn = fillimg(imgfn,"tmp.png",output_w,output_h)
loopfn = mkloop(imgfn)
wavdur = getdur(wavfn)
nreq = math.ceil(wavdur/60)
with open("catlist","wt") as f:
for n in range(nreq):
print(f"file '{loopfn}'",file=f)
run(["ffmpeg","-f","concat","-i","catlist","-i",wavfn,"-b:a","160k","-shortest","-c:v","copy",ofn],stdin=DEVNULL)
def getdur(ifn):
m = run(["ffprobe", "-v", "quiet", "-print_format", "json", "-show_format",ifn],stdin=DEVNULL,stdout=PIPE)
if m.returncode > 0:
print(f"#fail")
exit(1)
j = m.stdout.decode()
d = json.loads(j)
dur = float(d["format"]["duration"])
return dur
def mkloop(imgfn):
loopfn = "loop.mp4"
cmd=["ffmpeg", "-y", "-loop", "1", "-i", imgfn, "-t", "60", "-r", "24", "-pix_fmt", "yuv420p", "-vf", "scale=1920:1080", loopfn]
run(cmd,stdin=DEVNULL)
return loopfn
def fillimg(ifn,ofn,output_w,output_h):
im = Image.open(ifn)
iw,ih = im.size
tw,th = output_w,output_h
if tw == iw and th == ih:
return ifn
ir = iw/ih
tr = tw/th
if ir > tr:
# input is wider when scaled: scale by th/ih
sf = th/ih
ow = int(iw*sf)
oh = int(ih*sf)
im2 = im.resize((ow,oh),resample=Image.LANCZOS)
w2,h2 = im2.size
x2 = (w2-tw)//2
im3 = im2.crop((x2,0,tw,th))
else:
# input is higher when scaled (or equal), scale by tw/iw
sf = tw/iw
ow = int(iw*sf)
oh = int(ih*sf)
im2 = im.resize((ow,oh),resample=Image.LANCZOS)
w2,h2 = im2.size
y2 = (h2-th)//2
im3 = im2.crop((0,y2,tw,th))
im3.save(ofn)
return ofn
if __name__ == "__main__":
main()
Wav+Loop to Mp4
This is as above, but skips the image-to-loop stage. This takes a ready-made loop, repeats it as necessary, and then merges it and the wav. Usage:
wavloop2mp4 <audio file> <loop file> <output filename>
#!/usr/bin/env python
from subprocess import run, PIPE, DEVNULL
from glob import glob
from icecream import ic; ic.configureOutput(includeContext=True)
import sys
import json
import math
def main():
args = sys.argv[1:]
try:
wavfn, loopfn, ofn = args
except Exception:
print(f"{sys.argv[0]} wavfn loopfn ofn")
exit(1)
loopdur = getdur(loopfn)
wavdur = getdur(wavfn)
nreq = math.ceil(wavdur/loopdur)
with open("catlist","wt") as f:
for n in range(nreq):
print(f"file '{loopfn}'",file=f)
run(["ffmpeg","-n","-f","concat","-i","catlist","-i",wavfn,"-b:a","160k","-shortest","-c:v","copy",ofn],stdin=DEVNULL)
print(ofn)
def getdur(ifn):
m = run(["ffprobe", "-v", "quiet", "-print_format", "json", "-show_format",ifn],stdin=DEVNULL,stdout=PIPE)
if m.returncode > 0:
print(f"#fail")
exit(1)
j = m.stdout.decode()
d = json.loads(j)
dur = float(d["format"]["duration"])
return dur
if __name__ == "__main__":
main()