|
|
@@ -0,0 +1,94 @@
|
|
|
+#!/usr/bin/env python
|
|
|
+import argparse
|
|
|
+import errno
|
|
|
+from math import floor
|
|
|
+import ffmpeg
|
|
|
+import sys
|
|
|
+
|
|
|
+
|
|
|
+WAVES_BACKGROUND_IMAGE_RATIO = 0.2
|
|
|
+WAVES_IMAGE_RATIO = 0.15
|
|
|
+
|
|
|
+
|
|
|
+def main():
|
|
|
+ parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter, add_help=True)
|
|
|
+ parser.add_argument("--audio",
|
|
|
+ help="input audio filename", required=True)
|
|
|
+ parser.add_argument("--background",
|
|
|
+ help="visualization background filename", required=True)
|
|
|
+ parser.add_argument("--output",
|
|
|
+ help="output video filename", required=True)
|
|
|
+ parser.add_argument("--vis-color", nargs='*',
|
|
|
+ help="colors for visualization waveforms", required=False, default=["0xffffff"])
|
|
|
+ parser.add_argument("--background-color",
|
|
|
+ help="backgroundcolor for visualization waveforms", required=False, default="0x000000")
|
|
|
+
|
|
|
+ args, _ = parser.parse_known_args()
|
|
|
+
|
|
|
+ duration = get_audio_duration(args.audio)
|
|
|
+ (bg_height, bg_width) = get_image_resolution(args.background)
|
|
|
+ waves_height = floor(bg_height * WAVES_IMAGE_RATIO)
|
|
|
+ waves_background_height = floor(bg_height * WAVES_IMAGE_RATIO)
|
|
|
+
|
|
|
+ # Compile the waves and a background color
|
|
|
+ stream = ffmpeg.input(args.audio)
|
|
|
+ vis_colors = "|".join(args.vis_color)
|
|
|
+ vid_stream = (
|
|
|
+ stream
|
|
|
+ .filter("showwaves", s="%dx%d" % (bg_width, waves_height), mode="cline", colors=vis_colors)
|
|
|
+ .filter("format", "rgba")
|
|
|
+ .filter("colorchannelmixer", aa=0.9)
|
|
|
+ )
|
|
|
+ background_stream = (
|
|
|
+ ffmpeg.input("color=c=%s:s=%dx%d:d=%ss" % (args.background_color, bg_width, waves_background_height, duration), f="lavfi")
|
|
|
+ .filter("format", "rgba")
|
|
|
+ .filter("colorchannelmixer", aa=0.5)
|
|
|
+ )
|
|
|
+ waves_center_offset = floor((waves_background_height - waves_height)/2)
|
|
|
+ viz = ffmpeg.filter([background_stream, vid_stream], 'overlay', 0, waves_center_offset)
|
|
|
+ waves_background_center_offset = floor((bg_height - waves_background_height)/2)
|
|
|
+
|
|
|
+ # Overlay the waves stream on top of our static image
|
|
|
+ vid_stream = ffmpeg.filter([ffmpeg.input(args.background), viz], 'overlay', 0, waves_background_center_offset)
|
|
|
+ ffmpeg.output(stream.audio, vid_stream, args.output).run()
|
|
|
+
|
|
|
+
|
|
|
+# Get image resolution using ffprobe
|
|
|
+def get_image_resolution(image_filename):
|
|
|
+ metadata = get_metadata(image_filename)
|
|
|
+ height = metadata["streams"][0]["height"]
|
|
|
+ width = metadata["streams"][0]["width"]
|
|
|
+ return (height, width)
|
|
|
+
|
|
|
+
|
|
|
+# Get audio duration using ffprobe
|
|
|
+def get_audio_duration(audio_filename):
|
|
|
+ metadata = get_metadata(audio_filename)
|
|
|
+ return metadata["format"]["duration"]
|
|
|
+
|
|
|
+
|
|
|
+def get_metadata(filename):
|
|
|
+ metadata = ffmpeg.probe(filename)
|
|
|
+ return metadata
|
|
|
+
|
|
|
+
|
|
|
+if __name__ == "__main__":
|
|
|
+ try:
|
|
|
+ main()
|
|
|
+ except KeyboardInterrupt:
|
|
|
+ # The user asked the program to exit
|
|
|
+ sys.exit(1)
|
|
|
+ except IOError as e:
|
|
|
+ # When this program is used in a shell pipeline and an earlier program in
|
|
|
+ # the pipeline is terminated, we'll receive an EPIPE error. This is normal
|
|
|
+ # and just an indication that we should exit after processing whatever
|
|
|
+ # input we've received -- we don't consume standard input so we can just
|
|
|
+ # exit cleanly in that case.
|
|
|
+ if e.errno != errno.EPIPE:
|
|
|
+ raise
|
|
|
+
|
|
|
+ # We still exit with a non-zero exit code though in order to propagate the
|
|
|
+ # error code of the earlier process that was terminated.
|
|
|
+ sys.exit(1)
|
|
|
+
|
|
|
+ sys.exit(0)
|