From 0ef45d240e1bca14a3d7c3cc4137c7753b866308 Mon Sep 17 00:00:00 2001 From: WORD559 Date: Wed, 4 Jul 2018 11:24:37 +0100 Subject: [PATCH 01/14] Fixed a lot of indentation discrepancies, so everything should now use tabs and you should be able to expand/unexpand filters to change these locally (sorry about this, I just couldn't get anything to play nice with the code how it was) --- PPM.py | 95 +++++++++++++++++++++++++++++----------------------------- UGO.py | 26 ++++++++-------- 2 files changed, 60 insertions(+), 61 deletions(-) diff --git a/PPM.py b/PPM.py index cbd7594..f908bc4 100755 --- a/PPM.py +++ b/PPM.py @@ -479,23 +479,23 @@ class TMB: #if ppm: self = ppm out = ["PARA",#magic - DecAsc(self.AudioOffset-0x6a0, 4, True),#animation data size - DecAsc(self.AudioLenght, 4, True),#audio data size - DecAsc(self.FrameCount-1, 2, True),#frame count - "\x24\x00",#unknown - chr(self.Locked), "\0",#locked - DecAsc(self.ThumbnailFrameIndex, 2, True),#which frame is in the thumbnnail - self.OriginalAuthorName.encode("UTF-16LE") + "\0\0"*(11-len(self.OriginalAuthorName)),#Original Author Name - self.EditorAuthorName.encode("UTF-16LE") + "\0\0"*(11-len(self.EditorAuthorName)),#Editor Author Name - self.Username.encode("UTF-16LE") + "\0\0"*(11-len(self.Username)),#Username + DecAsc(self.AudioOffset-0x6a0, 4, True),#animation data size + DecAsc(self.AudioLenght, 4, True),#audio data size + DecAsc(self.FrameCount-1, 2, True),#frame count + "\x24\x00",#unknown + chr(self.Locked), "\0",#locked + DecAsc(self.ThumbnailFrameIndex, 2, True),#which frame is in the thumbnnail + self.OriginalAuthorName.encode("UTF-16LE") + "\0\0"*(11-len(self.OriginalAuthorName)),#Original Author Name + self.EditorAuthorName.encode("UTF-16LE") + "\0\0"*(11-len(self.EditorAuthorName)),#Editor Author Name + self.Username.encode("UTF-16LE") + "\0\0"*(11-len(self.Username)),#Username self.OriginalAuthorID.decode("HEX")[::-1],#OriginalAuthorID self.EditorAuthorID.decode("HEX")[::-1],#EditorAuthorID self.OriginalFilenameC,#OriginalFilename self.CurrentFilenameC,#CurrentFilename self.PreviousEditAuthorID.decode("HEX")[::-1],#EditorAuthorID self.PartialFilenameC,#PartialFilename - DecAsc(self.Date, 4, True),#Date in seconds - "\0\0",#padding + DecAsc(self.Date, 4, True),#Date in seconds + "\0\0",#padding self.PackThumbnail()]#thumbnail return "".join(out) @@ -592,26 +592,26 @@ def WriteImage(image, outputPath): return True def get_metadata(flipnote): - meta = { - u"Current filename":flipnote.CurrentFilename[:-3]+filetype, - u"Original filename":flipnote.OriginalFilename[:-3]+filetype, - u"Number of frames":flipnote.FrameCount, - u"Locked":flipnote.Locked, - u"Thumbnail frame index":(flipnote.ThumbnailFrameIndex+1), - u"Original author":flipnote.OriginalAuthorName, - u"Editor author":flipnote.EditorAuthorName, - u"Username":flipnote.Username, - u"Original author ID":flipnote.OriginalAuthorID, - u"Editor author ID":flipnote.EditorAuthorID, - u"Date(seconds since 1'st Jan 2000)":flipnote.Date, - u"Date":time.strftime("%H:%M %d/%m-%Y (faulty)", time.gmtime(epoch+flipnote.Date)), - } - if filetype == "ppm": - meta[u"Frame speed"]=flipnote.Framespeed - meta[u"Frame speed"]=flipnote.BGMFramespeed - meta[u"Looped"]=flipnote.Looped - - return meta + meta = { + u"Current filename":flipnote.CurrentFilename[:-3]+filetype, + u"Original filename":flipnote.OriginalFilename[:-3]+filetype, + u"Number of frames":flipnote.FrameCount, + u"Locked":flipnote.Locked, + u"Thumbnail frame index":(flipnote.ThumbnailFrameIndex+1), + u"Original author":flipnote.OriginalAuthorName, + u"Editor author":flipnote.EditorAuthorName, + u"Username":flipnote.Username, + u"Original author ID":flipnote.OriginalAuthorID, + u"Editor author ID":flipnote.EditorAuthorID, + u"Date(seconds since 1'st Jan 2000)":flipnote.Date, + u"Date":time.strftime("%H:%M %d/%m-%Y (faulty)", time.gmtime(epoch+flipnote.Date)), + } + if filetype == "ppm": + meta[u"Frame speed"]=flipnote.Framespeed + meta[u"Frame speed"]=flipnote.BGMFramespeed + meta[u"Looped"]=flipnote.Looped + + return meta if __name__ == '__main__': @@ -765,23 +765,22 @@ if __name__ == '__main__': f.write(newline.join(meta).encode("UTF-8")) f.close() elif sys.argv[1] == "-oa": - regex = re.compile(sys.argv[2]) - os.chdir(sys.argv[3]) - for filename in os.listdir("."): - epoch = time.mktime(time.struct_time([2000, 1, 1, 0, 0, 0, 5, 1, -1])) - - if not os.path.isfile(filename): - print "Error!\nSpecified file doesn't exist!" - sys.exit() - - filetype = "ppm" if filename[-3:] == "ppm" else "tmb" - flipnote = TMB().ReadFile(filename) if filetype == "tmb" else PPM().ReadFile(filename, ReadFrames=False) - if not flipnote: - continue + regex = re.compile(sys.argv[2]) + os.chdir(sys.argv[3]) + for filename in os.listdir("."): + epoch = time.mktime(time.struct_time([2000, 1, 1, 0, 0, 0, 5, 1, -1])) + if not os.path.isfile(filename): + print "Error!\nSpecified file doesn't exist!" + sys.exit() + + filetype = "ppm" if filename[-3:] == "ppm" else "tmb" + flipnote = TMB().ReadFile(filename) if filetype == "tmb" else PPM().ReadFile(filename, ReadFrames=False) + if not flipnote: + continue - meta = get_metadata(flipnote) - if regex.match(meta["Original author"]): - print filename - + meta = get_metadata(flipnote) + if regex.match(meta["Original author"]): + print filename + else: print "Error!\nThere's no such mode." diff --git a/UGO.py b/UGO.py index 4c0457b..949481a 100644 --- a/UGO.py +++ b/UGO.py @@ -51,19 +51,19 @@ def DecAsc(dec, length=None, LittleEndian=False):#Converts a decimal into an asc def zipalign(length, r=4): return length + (4 - length % r) if length % r else length def indentXML(elem, level=0):#"borrowed" from: http://effbot.org/zone/element-lib.htm#prettyprint - i = "\n" + level*"\t" - if len(elem): - if not elem.text or not elem.text.strip(): - elem.text = i + "\t" - if not elem.tail or not elem.tail.strip(): - elem.tail = i - for elem in elem: - indentXML(elem, level+1) - if not elem.tail or not elem.tail.strip(): - elem.tail = i - else: - if level and (not elem.tail or not elem.tail.strip()): - elem.tail = i + i = "\n" + level*"\t" + if len(elem): + if not elem.text or not elem.text.strip(): + elem.text = i + "\t" + if not elem.tail or not elem.tail.strip(): + elem.tail = i + for elem in elem: + indentXML(elem, level+1) + if not elem.tail or not elem.tail.strip(): + elem.tail = i + else: + if level and (not elem.tail or not elem.tail.strip()): + elem.tail = i #class UGO: class UGO: From 53d094b39e05246e77b91bdefa60343772da2615 Mon Sep 17 00:00:00 2001 From: WORD559 Date: Wed, 4 Jul 2018 12:37:50 +0100 Subject: [PATCH 02/14] Added ffmpeg detection in preparation for automated exporting --- PPM.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/PPM.py b/PPM.py index f908bc4..ad14650 100755 --- a/PPM.py +++ b/PPM.py @@ -12,14 +12,26 @@ # - Jsafive for supplying .tmb files # - Austin Burk, Midmad on hatena haiku and WDLmaster on hcs64.com for determining the sound codec # -import sys, wave, audioop, re#needs os and time aswell in CMD mode +import sys, wave, audioop, re, os #needs os and time aswell in CMD mode (UPDATE 2018/07/04: os used to get devnull for discarding subprocess output) import numpy as np +import subprocess # used for ffmpeg try: from PIL import Image hasPIL = True except ImportError: print "Warning: PIL not found, image extraction won't work!" hasPIL = False +# Test for ffmpeg by executing ffmpeg -h; an OSError indicates ffmpeg is not installed + +try: + with open(os.devnull,"w") as null: + subprocess.call(["ffmpeg","-h"], + stdout=null, + stderr=null) + hasffmpeg = True +except OSError: + print "Warning: ffmpeg not found, video export unavailable. Please make sure ffmepg is installed and can be accessed on your path." + hasffmpeg = False #helpers: def AscDec(ascii, LittleEndian=False):#Converts a ascii string into a decimal From 4c0c19f1d72ef79fe3cbf219f2945f4cfe8a902e Mon Sep 17 00:00:00 2001 From: WORD559 Date: Wed, 4 Jul 2018 13:07:15 +0100 Subject: [PATCH 03/14] Made many aspects of the program into functions so that they can be reused more efficiently in other commands --- PPM.py | 79 ++++++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 60 insertions(+), 19 deletions(-) diff --git a/PPM.py b/PPM.py index ad14650..79f3794 100755 --- a/PPM.py +++ b/PPM.py @@ -15,6 +15,7 @@ import sys, wave, audioop, re, os #needs os and time aswell in CMD mode (UPDATE 2018/07/04: os used to get devnull for discarding subprocess output) import numpy as np import subprocess # used for ffmpeg +import tempfile, shutil # temporary directory whilst exporting, shutil to clean up after try: from PIL import Image hasPIL = True @@ -625,6 +626,26 @@ def get_metadata(flipnote): return meta +def DumpFrames(flipnote,directory): + for i in xrange(flipnote.FrameCount): + print "Dumping frame #%i..." % (i+1), + WriteImage(flipnote.GetFrame(i), os.path.join(directory, "frame %s.png" % str(i+1).zfill(3))) + +def DumpSoundFiles(flipnote,directory,raw=False): + for i, data in enumerate(flipnote.SoundData): + if not data: continue + path = os.path.join(directory, ("BGM.wav", "SFX1.wav", "SFX2.wav", "SFX3.wav")[i]) + flipnote.GetSound(i, path) + + if raw: + f = open(path[:-3]+"bin", "wb") + f.write(data) + f.close() + +def DumpSFXUsage(flipnote,directory): + with open(os.path.join(directory, "SFX usage.txt"), "w") as f: + for i, (s1, s2, s3) in enumerate(flipnote.SFXUsage): + f.write("Frame %i:%s%s%s\n" % (i, " SFX1"*s1, " SFX2"*s2, " SFX3"*s3)) if __name__ == '__main__': print " == PPM.py ==" @@ -641,6 +662,7 @@ if __name__ == '__main__': print " -f: Extracts the frame(s) to " print " -s: Dumps the sound files to the folder " print " -S: Same as mode -s, but will also dump the raw sound data files" + print " -e: Exports the flipnote to an MKV" print " -m: Prints out the metadata. Can also write it to which also" print " supports unicode charactes." print " -oa: Seach a directory for an original author that matches the RegEx" @@ -691,11 +713,10 @@ if __name__ == '__main__': if not os.path.isdir(sys.argv[3]): print "Error!\nThe specified directory doesn't exist!" sys.exit() + + DumpFrames(flipnote,sys.argv[3]) - for i in xrange(flipnote.FrameCount): - print "Dumping frame #%i..." % (i+1), - WriteImage(flipnote.GetFrame(i), os.path.join(sys.argv[3], "frame %s.png" % str(i+1).zfill(3))) - print "Done!" + print "Done!" else: try: int(sys.argv[4]) @@ -731,22 +752,11 @@ if __name__ == '__main__': sys.exit() print "Converting the sound files...", - for i, data in enumerate(flipnote.SoundData): - if not data: continue - path = os.path.join(sys.argv[3], ("BGM.wav", "SFX1.wav", "SFX2.wav", "SFX3.wav")[i]) - flipnote.GetSound(i, path) - - if sys.argv[1] == "-S": - f = open(path[:-3]+"bin", "wb") - f.write(data) - f.close() + DumpSoundFiles(flipnote,sys.argv[3],raw=(sys.argv[1]=="-S")) print "Done!" print "Dumping the sound effect usage...", - f = open(os.path.join(sys.argv[3], "SFX usage.txt"), "w") - for i, (s1, s2, s3) in enumerate(flipnote.SFXUsage): - f.write("Frame %i:%s%s%s\n" % (i, " SFX1"*s1, " SFX2"*s2, " SFX3"*s3)) - f.close() + DumpSFXUsage(flipnote,sys.argv[3]) print "Done!" elif sys.argv[1] == "-m": epoch = time.mktime(time.struct_time([2000, 1, 1, 0, 0, 0, 5, 1, -1])) @@ -785,7 +795,7 @@ if __name__ == '__main__': print "Error!\nSpecified file doesn't exist!" sys.exit() - filetype = "ppm" if filename[-3:] == "ppm" else "tmb" + filetype = "ppm" if filename[-3:].lower() == "ppm" else "tmb" flipnote = TMB().ReadFile(filename) if filetype == "tmb" else PPM().ReadFile(filename, ReadFrames=False) if not flipnote: continue @@ -793,6 +803,37 @@ if __name__ == '__main__': meta = get_metadata(flipnote) if regex.match(meta["Original author"]): print filename - + + elif sys.argv[1] == "-e": + in_file = sys.argv[2] + out_file = sys.argv[3] + if out_file.lower()[-4:] != ".mkv": + out_file += ".mkv" + if not os.path.isfile(in_file): + print "Error!\nSpecified file doesn't exist!" + sys.exit() + if os.path.isfile(out_file): + print "Overwrite existing file?" + overwrite = "" + while overwrite != "y" and overwrite != "n": + overwrite = raw_input("(Y/N) ").lower() + if overwrite == "n": + print "Not overwriting; exiting." + sys.exit() + filetype = "ppm" if in_file[-3:].lower() == "ppm" else "tmb" + flipnote = TMB().ReadFile(in_file) if filetype == "tmb" else PPM().ReadFile(in_file, ReadFrames=True, ReadSound=True) + + # Make temp dir and dump the frames and sound here + tempdir = tempfile.mkdtemp() + print "Dumping the frames..." + DumpFrames(flipnote,tempdir) + print "Done!" + print "Dumping the sounds..." + DumpSoundFiles(flipnote,tempdir) + print "Done!" + print "Dumping SFX usage..." + DumpSFXUsage(flipnote,tempdir) + print "Done!" + else: print "Error!\nThere's no such mode." From 17085f316b1f2fceaa23a6e2cc8dc4e28bb3d6f8 Mon Sep 17 00:00:00 2001 From: WORD559 Date: Wed, 4 Jul 2018 14:01:48 +0100 Subject: [PATCH 04/14] Should now be able to export flipnotes with background music. Need to sort out adding sound effects and test with flipnotes where the music is shorter than the video --- PPM.py | 25 +++++++++++++++++++++---- speeds.md | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 speeds.md diff --git a/PPM.py b/PPM.py index 79f3794..9bfb3f5 100755 --- a/PPM.py +++ b/PPM.py @@ -605,6 +605,8 @@ def WriteImage(image, outputPath): return True def get_metadata(flipnote): + epoch = time.mktime(time.struct_time([2000, 1, 1, 0, 0, 0, 5, 1, -1])) + meta = { u"Current filename":flipnote.CurrentFilename[:-3]+filetype, u"Original filename":flipnote.OriginalFilename[:-3]+filetype, @@ -759,8 +761,6 @@ if __name__ == '__main__': DumpSFXUsage(flipnote,sys.argv[3]) print "Done!" elif sys.argv[1] == "-m": - epoch = time.mktime(time.struct_time([2000, 1, 1, 0, 0, 0, 5, 1, -1])) - if not os.path.isfile(sys.argv[2]): print "Error!\nSpecified file doesn't exist!" sys.exit() @@ -825,15 +825,32 @@ if __name__ == '__main__': # Make temp dir and dump the frames and sound here tempdir = tempfile.mkdtemp() + os.mkdir(tempdir+"/sounds") print "Dumping the frames..." DumpFrames(flipnote,tempdir) print "Done!" print "Dumping the sounds..." - DumpSoundFiles(flipnote,tempdir) + DumpSoundFiles(flipnote,tempdir+"/sounds") print "Done!" print "Dumping SFX usage..." - DumpSFXUsage(flipnote,tempdir) + DumpSFXUsage(flipnote,tempdir+"/sounds") print "Done!" + + # Now we need the metadata so we can look up the FPS + SPEEDS = [None,0.5,1,2,4,6,12,20,30] + print "Getting metadata..." + metadata = get_metadata(flipnote) + print "Done!" + speed = int(metadata["Frame speed"]) + fps = SPEEDS[speed] + print "Flipnote is speed {speed}, so {fps} FPS".format(speed=speed,fps=fps) + + # Now to make the video in ffmpeg + export_command = ["ffmpeg","-framerate",str(fps),"-start_number","1","-i","{path}/frame %03d.png".format(path=tempdir),"-i","{path}/sounds/BGM.wav".format(path=tempdir),"-c:v","libx264","-shortest",out_file] + if not os.path.isfile(tempdir+"/sounds/BGM.wav"): + export_command.pop(7) + export_command.pop(8) + subprocess.call(export_command) else: print "Error!\nThere's no such mode." diff --git a/speeds.md b/speeds.md new file mode 100644 index 0000000..dca36ba --- /dev/null +++ b/speeds.md @@ -0,0 +1,32 @@ +# Flipnote Speeds # + +This is some reference material taken from http://flipnote.wikia.com/wiki/Flipnote_Speed regarding flipnote speeds. + +## Flipnote Studio 3D ## + +|Speed|FPS| +|:===:|:=:| +|0 |0.2| +|1 |0.5| +|2 |1 | +|3 |2 | +|4 |4 | +|5 |6 | +|6 |8 | +|7 |12 | +|8 |20 | +|9 |22 | +|10 |29 | + +## Flipnote Studio DSi ## + +|Speed|FPS| +|:===:|:=:| +|1 |0.5| +|2 |1 | +|3 |2 | +|4 |4 | +|5 |6 | +|6 |12 | +|7 |20 | +|8 |30 | \ No newline at end of file From 09b665a7123dab944e5409f322cb69996bcda50d Mon Sep 17 00:00:00 2001 From: WORD559 Date: Wed, 4 Jul 2018 15:43:43 +0100 Subject: [PATCH 05/14] Works when audio is shorter than video --- PPM.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/PPM.py b/PPM.py index 9bfb3f5..b278be7 100755 --- a/PPM.py +++ b/PPM.py @@ -851,6 +851,11 @@ if __name__ == '__main__': export_command.pop(7) export_command.pop(8) subprocess.call(export_command) + + # Now I'll need to insert sound effects + # I can probably do this with ffmpeg by extending the audio with leading silence and using ffmpeg -i video.mkv -i gallop.ogg -filter_complex "[0:0][1:0] amix=inputs=2:duration=longest" main.ogg + # or at least something similar + # it'll need some experimentation. I'll have to do it another day. else: print "Error!\nThere's no such mode." From 2b370e379aadbe3b5fd6d1a27510119ea8a3ad4c Mon Sep 17 00:00:00 2001 From: Joshua Barrass Date: Thu, 5 Jul 2018 11:46:06 +0100 Subject: [PATCH 06/14] Should be pretty tables now --- speeds.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/speeds.md b/speeds.md index dca36ba..47de2bc 100644 --- a/speeds.md +++ b/speeds.md @@ -5,7 +5,7 @@ This is some reference material taken from http://flipnote.wikia.com/wiki/Flipno ## Flipnote Studio 3D ## |Speed|FPS| -|:===:|:=:| +|:---:|:-:| |0 |0.2| |1 |0.5| |2 |1 | @@ -21,7 +21,7 @@ This is some reference material taken from http://flipnote.wikia.com/wiki/Flipno ## Flipnote Studio DSi ## |Speed|FPS| -|:===:|:=:| +|:---:|:-:| |1 |0.5| |2 |1 | |3 |2 | @@ -29,4 +29,4 @@ This is some reference material taken from http://flipnote.wikia.com/wiki/Flipno |5 |6 | |6 |12 | |7 |20 | -|8 |30 | \ No newline at end of file +|8 |30 | From 16fe17eb1d38225a84d820b4f91b09bd41272b1a Mon Sep 17 00:00:00 2001 From: WORD559 Date: Thu, 5 Jul 2018 20:26:43 +0100 Subject: [PATCH 07/14] Inserting sound effects works! It's a bit heavy on disk usage (makes 2 new files in a temporary directory for every sound effect that needs to play, and then overwrites the video) and there's a couple of tests I need to run before I'd say it's 100% ready, but for now it works. --- PPM.py | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 55 insertions(+), 6 deletions(-) diff --git a/PPM.py b/PPM.py index b278be7..9e2f255 100755 --- a/PPM.py +++ b/PPM.py @@ -16,6 +16,7 @@ import sys, wave, audioop, re, os #needs os and time aswell in CMD mode (UPDATE import numpy as np import subprocess # used for ffmpeg import tempfile, shutil # temporary directory whilst exporting, shutil to clean up after +import time try: from PIL import Image hasPIL = True @@ -805,8 +806,17 @@ if __name__ == '__main__': print filename elif sys.argv[1] == "-e": + if not hasffmpeg: + print "Error!\nffmpeg is not installed." + sys.exit() in_file = sys.argv[2] out_file = sys.argv[3] + try: + sleep_time = int(sys.argv[4]) + except IndexError,ValueError: + sleep_time = 0 + print "Not sleeping." + if out_file.lower()[-4:] != ".mkv": out_file += ".mkv" if not os.path.isfile(in_file): @@ -846,16 +856,55 @@ if __name__ == '__main__': print "Flipnote is speed {speed}, so {fps} FPS".format(speed=speed,fps=fps) # Now to make the video in ffmpeg - export_command = ["ffmpeg","-framerate",str(fps),"-start_number","1","-i","{path}/frame %03d.png".format(path=tempdir),"-i","{path}/sounds/BGM.wav".format(path=tempdir),"-c:v","libx264","-shortest",out_file] + print "Exporting video with ffmpeg..." + export_command = ["ffmpeg","-framerate",str(fps),"-start_number","1","-i","{path}/frame %03d.png".format(path=tempdir),"-i","{path}/sounds/BGM.wav".format(path=tempdir),"-c:v","libx264","-shortest","-y",out_file] if not os.path.isfile(tempdir+"/sounds/BGM.wav"): + has_bgm = False export_command.pop(7) export_command.pop(8) + else: + has_bgm = True subprocess.call(export_command) + print "Done!" - # Now I'll need to insert sound effects - # I can probably do this with ffmpeg by extending the audio with leading silence and using ffmpeg -i video.mkv -i gallop.ogg -filter_complex "[0:0][1:0] amix=inputs=2:duration=longest" main.ogg - # or at least something similar - # it'll need some experimentation. I'll have to do it another day. - + # These are the ffmpeg commands I need for each sound effect + # The first generates a silent track, with variable length. This gets concatenated to the front of the sound effect. + silence_command = ["ffmpeg","-f","lavfi","-i","anullsrc=r=8192:cl=mono","-t","{length}","-f","wav","-y","{path}/silence.wav"] + # The second concatenates the silent track and the sound effect, producing a sound file that can be mixed into the video's audio track so that it plays at the correct time. + SFX_command = ["ffmpeg","-i","{path}/silence.wav","-i","{path}/sounds/{sfx}.wav","-filter_complex","[0:a] [1:a] concat=n=2:v=0:a=1","-y","{path}/sfx.wav"] + # The third mixes the silence+sound effect into the video file + merge_command = ["ffmpeg","-i",out_file,"-i","{path}/sfx.wav","-filter_complex","[0:a][1:a] amix=inputs=2:duration=longest","-c:v","copy","-y",out_file] + +## for sfx in ["SFX1","SFX2","SFX3"]: +## if not os.path.isfile("{path}/sounds/{sfx}.wav".format(path=tempdir,sfx=sfx)): +## print sfx+" does not exist." +## continue +## else: + + # Read in the sound effect usage data + print "Reading in sound effect usage..." + with open("{path}/sounds/SFX usage.txt".format(path=tempdir),"r") as sfx_usage_file: + sfx_usage = sfx_usage_file.read().split("\n") + print "Done!" + + # Iterate through the frames, checking if sound effects need to be added + for frame in range(len(sfx_usage)): + line = sfx_usage[frame] + # If a frame has an associated sound effect, get which sound effect to use + sfx = line.split(":")[1].strip() if line.strip() != "" else "" + if sfx != "": # If a sound effect must be played... + length = frame*fps + print "Adding "+sfx+" {length} seconds in.".format(length=length) + # ...run each command in series with the correct arguments + subprocess.call([i.format(path=tempdir,sfx=sfx,length=length) for i in silence_command]) + subprocess.call([i.format(path=tempdir,sfx=sfx) for i in SFX_command]) + subprocess.call([i.format(path=tempdir) for i in merge_command]) + time.sleep(sleep_time) # optional sleep -- in case you want to slow things down for HDD strain or reliability or something + + # Remove the temp dir and all files in it + print "Removing temporary directory..." + shutil.rmtree(tempdir) + print "Done!" + else: print "Error!\nThere's no such mode." From ba9b9ccc4cc689d4b0d4fb5e18346f46c3728cd8 Mon Sep 17 00:00:00 2001 From: WORD559 Date: Thu, 5 Jul 2018 20:35:30 +0100 Subject: [PATCH 08/14] Cleaned up the output by discarding ffmpeg's output --- PPM.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/PPM.py b/PPM.py index 9e2f255..ad5a58c 100755 --- a/PPM.py +++ b/PPM.py @@ -864,7 +864,8 @@ if __name__ == '__main__': export_command.pop(8) else: has_bgm = True - subprocess.call(export_command) + with open(os.devnull) as null: + subprocess.call(export_command,stdout=null,stderr=null) print "Done!" # These are the ffmpeg commands I need for each sound effect @@ -882,7 +883,7 @@ if __name__ == '__main__': ## else: # Read in the sound effect usage data - print "Reading in sound effect usage..." + print "Reading sound effect usage..." with open("{path}/sounds/SFX usage.txt".format(path=tempdir),"r") as sfx_usage_file: sfx_usage = sfx_usage_file.read().split("\n") print "Done!" @@ -894,11 +895,13 @@ if __name__ == '__main__': sfx = line.split(":")[1].strip() if line.strip() != "" else "" if sfx != "": # If a sound effect must be played... length = frame*fps - print "Adding "+sfx+" {length} seconds in.".format(length=length) + print "Adding "+sfx+" at {length} seconds into the video.".format(length=length) # ...run each command in series with the correct arguments - subprocess.call([i.format(path=tempdir,sfx=sfx,length=length) for i in silence_command]) - subprocess.call([i.format(path=tempdir,sfx=sfx) for i in SFX_command]) - subprocess.call([i.format(path=tempdir) for i in merge_command]) + with open(os.devnull,"w") as null: + subprocess.call([i.format(path=tempdir,sfx=sfx,length=length) for i in silence_command],stdout=null,stderr=null) + subprocess.call([i.format(path=tempdir,sfx=sfx) for i in SFX_command],stdout=null,stderr=null) + subprocess.call([i.format(path=tempdir) for i in merge_command],stdout=null,stderr=null) + print "Done!" time.sleep(sleep_time) # optional sleep -- in case you want to slow things down for HDD strain or reliability or something # Remove the temp dir and all files in it From 15a006cc5bfc0c0afb3f51725252af068a20ce76 Mon Sep 17 00:00:00 2001 From: WORD559 Date: Thu, 5 Jul 2018 21:08:28 +0100 Subject: [PATCH 09/14] Fixed exporting flipnotes that have no background music. --- PPM.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/PPM.py b/PPM.py index ad5a58c..ebe878d 100755 --- a/PPM.py +++ b/PPM.py @@ -859,11 +859,12 @@ if __name__ == '__main__': print "Exporting video with ffmpeg..." export_command = ["ffmpeg","-framerate",str(fps),"-start_number","1","-i","{path}/frame %03d.png".format(path=tempdir),"-i","{path}/sounds/BGM.wav".format(path=tempdir),"-c:v","libx264","-shortest","-y",out_file] if not os.path.isfile(tempdir+"/sounds/BGM.wav"): - has_bgm = False - export_command.pop(7) - export_command.pop(8) + print "No background music. Adding silent track..." + #has_bgm = False + export_command = ["ffmpeg","-framerate",str(fps),"-start_number","1","-i","{path}/frame %03d.png".format(path=tempdir),"-f","lavfi","-i","anullsrc=r=8192:cl=mono","-c:v","libx264","-shortest","-y",out_file] else: - has_bgm = True + #has_bgm = True + pass with open(os.devnull) as null: subprocess.call(export_command,stdout=null,stderr=null) print "Done!" From dd51daf5504e0e5ce779e2d47dec871a1c464efc Mon Sep 17 00:00:00 2001 From: WORD559 Date: Thu, 5 Jul 2018 21:35:04 +0100 Subject: [PATCH 10/14] ffmpeg writes to a temp file and then removes the original to avoid corruption while it is processing, and now uses WAV PCM as the audio format to try to maximise the quality --- PPM.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/PPM.py b/PPM.py index ebe878d..2710603 100755 --- a/PPM.py +++ b/PPM.py @@ -857,11 +857,11 @@ if __name__ == '__main__': # Now to make the video in ffmpeg print "Exporting video with ffmpeg..." - export_command = ["ffmpeg","-framerate",str(fps),"-start_number","1","-i","{path}/frame %03d.png".format(path=tempdir),"-i","{path}/sounds/BGM.wav".format(path=tempdir),"-c:v","libx264","-shortest","-y",out_file] + export_command = ["ffmpeg","-framerate",str(fps),"-start_number","1","-i","{path}/frame %03d.png".format(path=tempdir),"-i","{path}/sounds/BGM.wav".format(path=tempdir),"-c:v","libx264","-c:a","pcm_s16le","-shortest","-y",out_file] if not os.path.isfile(tempdir+"/sounds/BGM.wav"): print "No background music. Adding silent track..." #has_bgm = False - export_command = ["ffmpeg","-framerate",str(fps),"-start_number","1","-i","{path}/frame %03d.png".format(path=tempdir),"-f","lavfi","-i","anullsrc=r=8192:cl=mono","-c:v","libx264","-shortest","-y",out_file] + export_command = ["ffmpeg","-framerate",str(fps),"-start_number","1","-i","{path}/frame %03d.png".format(path=tempdir),"-f","lavfi","-i","anullsrc=r=8192:cl=mono","-c:v","libx264","-c:a","pcm_s16le","-shortest","-y",out_file] else: #has_bgm = True pass @@ -875,7 +875,7 @@ if __name__ == '__main__': # The second concatenates the silent track and the sound effect, producing a sound file that can be mixed into the video's audio track so that it plays at the correct time. SFX_command = ["ffmpeg","-i","{path}/silence.wav","-i","{path}/sounds/{sfx}.wav","-filter_complex","[0:a] [1:a] concat=n=2:v=0:a=1","-y","{path}/sfx.wav"] # The third mixes the silence+sound effect into the video file - merge_command = ["ffmpeg","-i",out_file,"-i","{path}/sfx.wav","-filter_complex","[0:a][1:a] amix=inputs=2:duration=longest","-c:v","copy","-y",out_file] + merge_command = ["ffmpeg","-i",out_file,"-i","{path}/sfx.wav","-filter_complex","[0:a][1:a] amix=inputs=2:duration=longest","-c:a","pcm_s16le","-c:v","copy","-y","{path}/temp_out.mkv"] ## for sfx in ["SFX1","SFX2","SFX3"]: ## if not os.path.isfile("{path}/sounds/{sfx}.wav".format(path=tempdir,sfx=sfx)): @@ -902,6 +902,8 @@ if __name__ == '__main__': subprocess.call([i.format(path=tempdir,sfx=sfx,length=length) for i in silence_command],stdout=null,stderr=null) subprocess.call([i.format(path=tempdir,sfx=sfx) for i in SFX_command],stdout=null,stderr=null) subprocess.call([i.format(path=tempdir) for i in merge_command],stdout=null,stderr=null) + os.remove(out_file) + shutil.move("{path}/temp_out.mkv".format(path=tempdir),out_file) print "Done!" time.sleep(sleep_time) # optional sleep -- in case you want to slow things down for HDD strain or reliability or something From 1b552a9e304150aa228e027b8addd9659bdfe84f Mon Sep 17 00:00:00 2001 From: WORD559 Date: Thu, 5 Jul 2018 22:04:35 +0100 Subject: [PATCH 11/14] Changed an erroneous multiply to a divide (fps * frame != time) --- PPM.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PPM.py b/PPM.py index 2710603..82fd688 100755 --- a/PPM.py +++ b/PPM.py @@ -895,7 +895,7 @@ if __name__ == '__main__': # If a frame has an associated sound effect, get which sound effect to use sfx = line.split(":")[1].strip() if line.strip() != "" else "" if sfx != "": # If a sound effect must be played... - length = frame*fps + length = frame//fps print "Adding "+sfx+" at {length} seconds into the video.".format(length=length) # ...run each command in series with the correct arguments with open(os.devnull,"w") as null: From fd375d5e66a1ad9235b011aabbee5790ba1492f8 Mon Sep 17 00:00:00 2001 From: WORD559 Date: Thu, 5 Jul 2018 22:40:39 +0100 Subject: [PATCH 12/14] Floating point division for silence length so that the sound effect definitely plays at the correct point --- PPM.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PPM.py b/PPM.py index 82fd688..aee2c7e 100755 --- a/PPM.py +++ b/PPM.py @@ -895,7 +895,7 @@ if __name__ == '__main__': # If a frame has an associated sound effect, get which sound effect to use sfx = line.split(":")[1].strip() if line.strip() != "" else "" if sfx != "": # If a sound effect must be played... - length = frame//fps + length = frame/float(fps) print "Adding "+sfx+" at {length} seconds into the video.".format(length=length) # ...run each command in series with the correct arguments with open(os.devnull,"w") as null: From 97c3f67d14589ba0229ef9a679969983d47006b6 Mon Sep 17 00:00:00 2001 From: WORD559 Date: Fri, 6 Jul 2018 00:09:02 +0100 Subject: [PATCH 13/14] Added option to force flipnote speed if you want to, and also fixed the difference between "Frame speed" and "BGM Frame speed" so that I can fix BGM that has been speed shifted. All works now. --- PPM.py | 37 +++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/PPM.py b/PPM.py index aee2c7e..bf0fdef 100755 --- a/PPM.py +++ b/PPM.py @@ -122,12 +122,13 @@ ThumbPalette = (0xFEFEFEFF,#0 # To get a colored image of a frame, use instance.GetFrame() # - instance.SoundData: A list contaning the raw data of the BGM and the 3 sound effects. Only available if ReadSound was true set to true class PPM: - def __init__(self): + def __init__(self,forced_speed=None): self.Loaded = [False, False, False]#(Meta, Frames, Sound) self.Frames = None self.Thumbnail = None self.RawThumbnail = None self.SoundData = None + self.forced_speed = forced_speed def ReadFile(self, path, DecodeThumbnail=False, ReadFrames=True, ReadSound=False):#Load: (thumbnail, frames, sound) f = open(path, "rb") ret = self.Read(f.read(), DecodeThumbnail, ReadFrames, ReadSound) @@ -181,9 +182,14 @@ class PPM: AscDec(data[AddPadding(AudioOffset+self.FrameCount, 4)+ 4:AddPadding(AudioOffset+self.FrameCount, 4)+ 8], True),#SFX1 AscDec(data[AddPadding(AudioOffset+self.FrameCount, 4)+ 8:AddPadding(AudioOffset+self.FrameCount, 4)+12], True),#SFX2 AscDec(data[AddPadding(AudioOffset+self.FrameCount, 4)+12:AddPadding(AudioOffset+self.FrameCount, 4)+16], True))#SFX3 - self.Framespeed = 8 - ord(data[AddPadding(AudioOffset+self.FrameCount, 4) + 16]) + if self.forced_speed == None: + self.Framespeed = 8 - ord(data[AddPadding(AudioOffset+self.FrameCount, 4) + 16]) + else: + self.Framespeed = self.forced_speed self.BGMFramespeed = 8 - ord(data[AddPadding(AudioOffset+self.FrameCount, 4) + 17])#framespeed when the bgm was recorded +## self.BGMFramespeed = self.forced_speed + #Read the Frames: if ReadFrames: self.Frames = []#self.Frames[frame] = [inverted(bool), (color1(0-2), color2(0-2)), frame[layer, x, y] = bool] @@ -624,7 +630,7 @@ def get_metadata(flipnote): } if filetype == "ppm": meta[u"Frame speed"]=flipnote.Framespeed - meta[u"Frame speed"]=flipnote.BGMFramespeed + meta[u"BGM Frame speed"]=flipnote.BGMFramespeed meta[u"Looped"]=flipnote.Looped return meta @@ -812,10 +818,15 @@ if __name__ == '__main__': in_file = sys.argv[2] out_file = sys.argv[3] try: - sleep_time = int(sys.argv[4]) - except IndexError,ValueError: + sleep_time = int(sys.argv[sys.argv.index("--sleep")+1]) + except (IndexError,ValueError): sleep_time = 0 print "Not sleeping." + try: + forced_speed = int(sys.argv[sys.argv.index("--speed")+1]) + print "Using forced speed "+str(forced_speed) + except (IndexError,ValueError): + forced_speed = None if out_file.lower()[-4:] != ".mkv": out_file += ".mkv" @@ -831,7 +842,7 @@ if __name__ == '__main__': print "Not overwriting; exiting." sys.exit() filetype = "ppm" if in_file[-3:].lower() == "ppm" else "tmb" - flipnote = TMB().ReadFile(in_file) if filetype == "tmb" else PPM().ReadFile(in_file, ReadFrames=True, ReadSound=True) + flipnote = TMB().ReadFile(in_file) if filetype == "tmb" else PPM(forced_speed).ReadFile(in_file, ReadFrames=True, ReadSound=True) # Make temp dir and dump the frames and sound here tempdir = tempfile.mkdtemp() @@ -869,6 +880,20 @@ if __name__ == '__main__': subprocess.call(export_command,stdout=null,stderr=null) print "Done!" + # If the audio has been sped up, we have to do it again manually + bgm_speed = int(metadata["BGM Frame speed"]) + if bgm_speed != speed: + print "Background music speed must be modified!" + original_rate = 8192 + newrate = 8192*(float(fps)/SPEEDS[bgm_speed]) + print "Using new rate: "+str(newrate) + speed_change_command = ["ffmpeg","-i","{path}/sounds/BGM.wav".format(path=tempdir),"-filter_complex","asetrate={rate}".format(rate=newrate),"-i",out_file,"-map","0:a","-map","1:v","-vcodec","copy","-acodec","pcm_s16le","{path}/temp_out.mkv".format(path=tempdir)] + with open(os.devnull,"w") as null: + subprocess.call(speed_change_command,stdout=null,stderr=null) + os.remove(out_file) + shutil.move("{path}/temp_out.mkv".format(path=tempdir),out_file) + print "Done!" + # These are the ffmpeg commands I need for each sound effect # The first generates a silent track, with variable length. This gets concatenated to the front of the sound effect. silence_command = ["ffmpeg","-f","lavfi","-i","anullsrc=r=8192:cl=mono","-t","{length}","-f","wav","-y","{path}/silence.wav"] From 9c1ffcd2901901a651164746bc969a83ea7f3d81 Mon Sep 17 00:00:00 2001 From: WORD559 Date: Fri, 6 Jul 2018 01:22:48 +0100 Subject: [PATCH 14/14] ffmpeg now uses the veryslow libx264 preset to give better compression (the videos are so short it doesn't make much difference to the time) and when mixing sound effects the output volume from amix is doubled to compensate for the volume halving as the sound effects are mixed --- PPM.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/PPM.py b/PPM.py index bf0fdef..b84b314 100755 --- a/PPM.py +++ b/PPM.py @@ -868,11 +868,11 @@ if __name__ == '__main__': # Now to make the video in ffmpeg print "Exporting video with ffmpeg..." - export_command = ["ffmpeg","-framerate",str(fps),"-start_number","1","-i","{path}/frame %03d.png".format(path=tempdir),"-i","{path}/sounds/BGM.wav".format(path=tempdir),"-c:v","libx264","-c:a","pcm_s16le","-shortest","-y",out_file] + export_command = ["ffmpeg","-framerate",str(fps),"-start_number","1","-i","{path}/frame %03d.png".format(path=tempdir),"-i","{path}/sounds/BGM.wav".format(path=tempdir),"-c:v","libx264","-preset","veryslow","-c:a","pcm_s16le","-shortest","-y",out_file] if not os.path.isfile(tempdir+"/sounds/BGM.wav"): print "No background music. Adding silent track..." #has_bgm = False - export_command = ["ffmpeg","-framerate",str(fps),"-start_number","1","-i","{path}/frame %03d.png".format(path=tempdir),"-f","lavfi","-i","anullsrc=r=8192:cl=mono","-c:v","libx264","-c:a","pcm_s16le","-shortest","-y",out_file] + export_command = ["ffmpeg","-framerate",str(fps),"-start_number","1","-i","{path}/frame %03d.png".format(path=tempdir),"-f","lavfi","-i","anullsrc=r=8192:cl=mono","-c:v","libx264","-preset","veryslow","-c:a","pcm_s16le","-shortest","-y",out_file] else: #has_bgm = True pass @@ -900,8 +900,9 @@ if __name__ == '__main__': # The second concatenates the silent track and the sound effect, producing a sound file that can be mixed into the video's audio track so that it plays at the correct time. SFX_command = ["ffmpeg","-i","{path}/silence.wav","-i","{path}/sounds/{sfx}.wav","-filter_complex","[0:a] [1:a] concat=n=2:v=0:a=1","-y","{path}/sfx.wav"] # The third mixes the silence+sound effect into the video file - merge_command = ["ffmpeg","-i",out_file,"-i","{path}/sfx.wav","-filter_complex","[0:a][1:a] amix=inputs=2:duration=longest","-c:a","pcm_s16le","-c:v","copy","-y","{path}/temp_out.mkv"] + merge_command = ["ffmpeg","-i",out_file,"-i","{path}/sfx.wav","-filter_complex","[0:a][1:a] amix=inputs=2:duration=longest:dropout_transition={video_length},volume=2","-c:a","pcm_s16le","-c:v","copy","-max_muxing_queue_size","1024","-y","{path}/temp_out.mkv"] +## normalise_command = ["ffmpeg","-i",out_file,"-filter_complex","dynaudnorm","{path}/temp_out.mkv".format(path=tempdir)] ## for sfx in ["SFX1","SFX2","SFX3"]: ## if not os.path.isfile("{path}/sounds/{sfx}.wav".format(path=tempdir,sfx=sfx)): ## print sfx+" does not exist." @@ -926,12 +927,17 @@ if __name__ == '__main__': with open(os.devnull,"w") as null: subprocess.call([i.format(path=tempdir,sfx=sfx,length=length) for i in silence_command],stdout=null,stderr=null) subprocess.call([i.format(path=tempdir,sfx=sfx) for i in SFX_command],stdout=null,stderr=null) - subprocess.call([i.format(path=tempdir) for i in merge_command],stdout=null,stderr=null) - os.remove(out_file) - shutil.move("{path}/temp_out.mkv".format(path=tempdir),out_file) + subprocess.call([i.format(path=tempdir,video_length=fps*len(sfx_usage)) for i in merge_command],stdout=null,stderr=null) + os.remove(out_file) + shutil.move("{path}/temp_out.mkv".format(path=tempdir),out_file) print "Done!" time.sleep(sleep_time) # optional sleep -- in case you want to slow things down for HDD strain or reliability or something +## subprocess.call(normalise_command) +## os.remove(out_file) +## shutil.move("{path}/temp_out.mkv".format(path=tempdir),out_file) + + # Remove the temp dir and all files in it print "Removing temporary directory..." shutil.rmtree(tempdir)