script.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. #!/usr/bin/env python
  2. import argparse
  3. import errno
  4. from math import floor
  5. import ffmpeg
  6. import sys
  7. # Arg validation for floats
  8. def restricted_float(x):
  9. try:
  10. x = float(x)
  11. except ValueError:
  12. raise argparse.ArgumentTypeError("%r not a floating-point literal" % (x,))
  13. if x < 0.0 or x > 1.0:
  14. raise argparse.ArgumentTypeError("%r not in range [0.0, 1.0]"%(x,))
  15. return x
  16. def main():
  17. parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter, add_help=True)
  18. parser.add_argument("--audio",
  19. help="input audio filename", required=True)
  20. parser.add_argument("--background",
  21. help="visualization background filename", required=True)
  22. parser.add_argument("--output",
  23. help="output video filename", required=True)
  24. parser.add_argument("--vis-background-to-vid-ratio", type=restricted_float, default=0.2,
  25. help="ratio of visualization background height to input image height (0.0-1.0)", required=False)
  26. parser.add_argument("--vis-waves-to-vid-ratio", type=restricted_float, default=0.15,
  27. help="ratio of visualization waves height to input image height (0.0-1.0)", required=False)
  28. parser.add_argument("--vis-color", nargs='*', required=False, default=["0xffffff"],
  29. help="colors for visualization waveforms")
  30. parser.add_argument("--vis-color-opacity", type=restricted_float, default=0.9,
  31. help="opacity of vis colors (0.0-1.0)", required=False)
  32. parser.add_argument("--background-color", required=False, default="0x000000",
  33. help="background color for visualization waveforms")
  34. parser.add_argument("--background-color-opacity", type=restricted_float, default=0.5,
  35. help="opacity for visualization background color (0.0-1.0)", required=False)
  36. args, _ = parser.parse_known_args()
  37. # Get metadata for visualization
  38. duration = get_audio_duration(args.audio)
  39. (bg_height, bg_width) = get_image_resolution(args.background)
  40. waves_height = floor(bg_height * args.vis_waves_to_vid_ratio)
  41. waves_background_height = floor(bg_height * args.vis_background_to_vid_ratio)
  42. # Compile the waves and a background color
  43. stream = ffmpeg.input(args.audio)
  44. vis_colors = "|".join(args.vis_color)
  45. vid_stream = get_audio_waveforms(stream, bg_width, waves_height, vis_colors, args.vis_color_opacity)
  46. background_stream = generate_background_color(bg_width, waves_background_height, args.background_color,
  47. args.background_color_opacity, duration)
  48. waves_center_offset = floor((waves_background_height - waves_height)/2)
  49. viz = ffmpeg.filter([background_stream, vid_stream], 'overlay', 0, waves_center_offset)
  50. waves_background_center_offset = floor((bg_height - waves_background_height)/2)
  51. # Overlay the waves stream on top of our static image
  52. vid_stream = ffmpeg.filter([ffmpeg.input(args.background), viz], 'overlay', 0, waves_background_center_offset)
  53. ffmpeg.output(stream.audio, vid_stream, args.output).run()
  54. # Generate a static color background video stream
  55. def generate_background_color(width, height, color, opacity, duration_in_seconds):
  56. return (
  57. ffmpeg.input("color=c=%s:s=%dx%d:d=%ss" % (color, width, height, duration_in_seconds), f="lavfi")
  58. .filter("format", "rgba")
  59. .filter("colorchannelmixer", aa=opacity)
  60. )
  61. # Given an input AV source, generate visualization waves
  62. def get_audio_waveforms(av_stream, width, height, colors, opacity):
  63. return (
  64. av_stream
  65. .filter("showwaves", s="%dx%d" % (width, height), mode="cline", colors=colors)
  66. .filter("format", "rgba")
  67. .filter("colorchannelmixer", aa=opacity)
  68. )
  69. # Get image resolution using ffprobe
  70. def get_image_resolution(image_filename):
  71. metadata = get_metadata(image_filename)
  72. height = metadata["streams"][0]["height"]
  73. width = metadata["streams"][0]["width"]
  74. return (height, width)
  75. # Get audio duration using ffprobe
  76. def get_audio_duration(audio_filename):
  77. metadata = get_metadata(audio_filename)
  78. return metadata["format"]["duration"]
  79. # Get metadata about file from ffprob
  80. def get_metadata(filename):
  81. metadata = ffmpeg.probe(filename)
  82. return metadata
  83. if __name__ == "__main__":
  84. try:
  85. main()
  86. except KeyboardInterrupt:
  87. # The user asked the program to exit
  88. sys.exit(1)
  89. except IOError as e:
  90. # When this program is used in a shell pipeline and an earlier program in
  91. # the pipeline is terminated, we'll receive an EPIPE error. This is normal
  92. # and just an indication that we should exit after processing whatever
  93. # input we've received -- we don't consume standard input so we can just
  94. # exit cleanly in that case.
  95. if e.errno != errno.EPIPE:
  96. raise
  97. # We still exit with a non-zero exit code though in order to propagate the
  98. # error code of the earlier process that was terminated.
  99. sys.exit(1)
  100. sys.exit(0)