From e60fc40807acfd5fad891e01ab6535ca50cfe76f Mon Sep 17 00:00:00 2001 From: jstma Date: Sat, 20 May 2023 14:25:21 -0700 Subject: [PATCH] First crack at porting kale good's count in beats (pull #46) * the beats are generated as files in /tmp/ly2video.../beats/ this is not the way ly2video does it now. * the count in stops, but then there is a delay before the score midi begins. * must supply a ttf file, but it wouldn't hurt to include an open source ttf font as a reasonable default. --- ly2video/cli.py | 107 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 106 insertions(+), 1 deletion(-) diff --git a/ly2video/cli.py b/ly2video/cli.py index 3795c06..ace76d8 100755 --- a/ly2video/cli.py +++ b/ly2video/cli.py @@ -327,6 +327,54 @@ def generateTitleFrame(titleText, width, height, ttfFile): return titleScreen +def generateBeats(width, height, ttfFile, beatstempo, beats, fps): + """ + Generates frames with count-in beats. + + Params: + - width: pixel width of frames (and video) + - height: pixel height of frames (and video) + - ttfFile: path to TTF file to use for title text + - fps: frame rate (frames per second) of final video + - beatstempo: tempo + - beats number of count-in beats + """ + + #I had the zero displayed because it otherwise seemed very abrupt. A click-track could help this. A nice feature would be option to make last note red (or other color). + + # save folder for frames + if not os.path.exists("beats"): + os.mkdir("beats") + + totalFrames = int(round((60.000/beatstempo) * fps)) + + for currentBeat in range(beats+1): + + printBeat = currentBeat + + # create image of beat screen + beatScreen = Image.new("RGB", (width, height), (255,255,255)) + # it will draw text on titleScreen + drawer = ImageDraw.Draw(beatScreen) + + progress("Beats: ly2video will generate approx. %d frames." % totalFrames) + progress("Beats: %s." % ttfFile) + # font for beat args - font type, size + nameFont = ImageFont.truetype(ttfFile, int(height / 10)) + + # args - position of left upper corner of rectangle (around text), text, font and color (black) + drawer.text(((width - nameFont.getsize("%d" % printBeat)[0]) / 2, + (height - nameFont.getsize("%d" % printBeat)[1]) / 2 - height / 25), + "%d" % printBeat, font=nameFont, fill=(0,0,0)) + + # generate needed number of frames (= (60/tempo) * fps) + for frameNum in xrange(totalFrames): + beatScreen.save(tmpPath("beats", "frame%04d.png" % ((currentBeat*totalFrames)+frameNum))) + + progress("Beats: Generating title screen has ended. (%d/%d)" % + (totalFrames, totalFrames)) + + return 0 def staffSpacesToPixels(ss, dpi): staffSpacePoints = GLOBAL_STAFF_SIZE / 4 @@ -946,6 +994,24 @@ def parseOptions(): help="show program version", action="store_true", default=False) + group_countin = parser.add_argument_group(title="Count-in Beats") + + group_countin.add_argument( + "--count-in", dest="beatsAtStart", + help='adds count in beat screens at the start of video', + action="store_true", default=False) + #would be best if these next options were pulled from lily file. This would require something to read time signature and recognize compound time so as to divide prefix by 3 for proper amount of count-in beats(6/8 = 2 count in beats). + group_countin.add_argument( + "--beats", dest="beats", + help='number of count-in beats', + type=int, metavar="BEATS", default=False) + group_countin.add_argument( + "--tempo", dest="beatstempo", + help='tempo of music -needs compound meter explained', + type=float, metavar="TEMPO", default=False) + #not sure if dest='tempo' would conflict, used beatstempo instead. + #another nice option would be for multiple measures of count-in. + if len(sys.argv) == 1: parser.print_help() sys.exit(0) @@ -955,7 +1021,7 @@ def parseOptions(): if options.showVersion: showVersion() - if options.titleAtStart and options.titleTtfFile is None: + if (options.titleAtStart or options.beatsAtStart) and options.titleTtfFile is None: fatal("Must specify --title-ttf=FONT-FILE with --title-at-start.") if options.debug: @@ -1198,6 +1264,34 @@ def generateSilentVideo(ffmpeg, fps, quality, desiredDuration, name, srcFrame): output_divider_line() return out +def imageFileToBytes(file): + f = BytesIO() + image = Image.open(file) + image.save(f, format="BMP") + return f.getvalue() + +def generateBeatVideo(ffmpeg, fps, quality, beatstempo, beats, name, srcFrame): + beatFrames = sorted(os.listdir('beats')) + out = tmpPath('%s.mpg' % name) + frames = int(((beats / beatstempo) * 60) * fps) + trueDuration = float(frames) / fps + progress("Generating beat video %s, duration %fs\n" % + (out, trueDuration)) + silentAudio = generateSilence(name, trueDuration) + cmd = [ + ffmpeg, + "-nostdin", + "-f", "image2pipe", + "-r", str(fps), + "-i", "-", + "-i", silentAudio, + "-q:v", quality, + "-f", "avi", + out + ] + safeRunInput(cmd, inputs=(imageFileToBytes('beats/'+frame) for frame in beatFrames), exitcode=16) + output_divider_line() + return out def generateVideo(ffmpeg, options, wavPath, titleText, frameWriter, outputFile): fps = float(options.fps) @@ -1230,6 +1324,17 @@ def generateVideo(ffmpeg, options, wavPath, titleText, frameWriter, outputFile): 'title', titleFrame) videos.insert(0, video) + if options.beatsAtStart: + beatFrames = generateBeats(options.width, options.height, + options.titleTtfFile, options.beatstempo, options.beats, fps) + + output_divider_line() + + video = generateBeatVideo(ffmpeg, fps, quality, + options.beatstempo, options.beats, + 'beats', beatFrames) + videos.insert(0, video) + if len(videos) == 1: os.rename(videos[0], outputFile) else: