Import project.
This commit is contained in:
parent
4d5713fed1
commit
28e37f3440
37 changed files with 7266 additions and 0 deletions
BIN
bin/python32.dll
Normal file
BIN
bin/python32.dll
Normal file
Binary file not shown.
BIN
bin/vfr.exe
Normal file
BIN
bin/vfr.exe
Normal file
Binary file not shown.
18
bin/vfr/.gitignore
vendored
Normal file
18
bin/vfr/.gitignore
vendored
Normal file
|
@ -0,0 +1,18 @@
|
|||
*~
|
||||
*.pyc
|
||||
*.diff
|
||||
*.converted.txt
|
||||
*.mkv
|
||||
*.mka
|
||||
*.ass
|
||||
*.ffindex
|
||||
*.xml
|
||||
*.qpfile
|
||||
*.qpf
|
||||
Thumbs.db
|
||||
*.bak
|
||||
.project
|
||||
.pydevproject
|
||||
.settings
|
||||
test/result*
|
||||
test/chap*
|
21
bin/vfr/LICENSE
Normal file
21
bin/vfr/LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
The MIT License
|
||||
|
||||
Copyright (c) 2010 Ricardo Constantino
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
68
bin/vfr/README
Normal file
68
bin/vfr/README
Normal file
|
@ -0,0 +1,68 @@
|
|||
vfr.py
|
||||
======
|
||||
|
||||
Inspired on: Daiz's AutoMKVChapters, TheFluff's split_aud, BD_Chapters
|
||||
|
||||
Needs: Python 3; MkvToolNix (for audio trimming)
|
||||
|
||||
What it does
|
||||
------------
|
||||
|
||||
* Reads the first line of uncommented Trims from an .avs;
|
||||
* Uses timecodes files to get each trim's frame's timestamp;
|
||||
* Offsets the trims accordingly;
|
||||
* Creates a basic xml with Matroska chapters, x264 chapters if ending in 'x264.txt' or OGM chapters if any other extension is used;
|
||||
* Creates a qpfile to use with x264;
|
||||
* Cuts and merges audio (as per split_aud, only using v2 timecodes instead of expecting cfr) (all options work as split_aud);
|
||||
* No longer needs tcConv but converts v1 timecodes to v2 internally;
|
||||
* If requested, can output v2 timecodes from v1 and fps parsing. If --ofps is being used, v2 timecodes will use it;
|
||||
* Can output a qpfile with converted frames meant to be used for an ivtc'd encode using non-ivtc'd frames (feature inspired by automkvchapters) (not completely accurate, obviously);
|
||||
* Using FFmpegsource's CorrectNTSCRationalFramerate, this is actually more precise in the v2 timecodes it produces than tcConv;
|
||||
* Accepts AutoMKVChapters-like templates.
|
||||
|
||||
Only the .avs with trims is required for vfr.py to run. You can use -v and/or --test to debug the script. All other options and arguments are optional.
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
vfr.py -i audio.aac -o audio.cut.mka -f 30/1.001 -l tRim -c chapters.xml -t template.txt \
|
||||
-n chnames.txt -q qpfile.qpf -vmr --ofps 24/1.001 --timecodes v2.txt --test trims.avs outtrims.avs
|
||||
|
||||
Required:
|
||||
trims.avs = Gets first uncommented line starting with trims from this Avisynth script
|
||||
|
||||
Optional:
|
||||
-i = Audio to be cut (takes whatever mkvmerge takes)
|
||||
-o = Cut audio inside .mka
|
||||
Default: input.cut.mka
|
||||
-d = Manually set delay time for input audio (can be negative)
|
||||
-b = Reverse parsing of .avs (from bottom to top)
|
||||
-f = Frames per second or timecodes file if vfr input
|
||||
(takes "25", "24000/1001", "30000:1001", "24/1.001" and "30:1.001" as cfr input)
|
||||
Default: 30000/1001
|
||||
-l = Look for a line starting with a case-sensitive trim() or case-insensitive comment succeeding the trims, interpreted as a regular expression.
|
||||
Default: case insensitive trim
|
||||
-g = Specify directly the line used
|
||||
-c = Chapters file. If extension is 'xml', outputs MKV Chapters;
|
||||
if extension is 'x264.txt', outputs x264 Chapters; else, outputs OGM Chapters
|
||||
-n = Text file with chapter names, one per line; assumed to be UTF-8 without BOM
|
||||
-q = QPFile for use in x264; will use --ofps frames
|
||||
-t = Template file for advanced Matroska chapters
|
||||
-v = Verbose mode
|
||||
-m = Merge split audio files
|
||||
-r = Remove split audio files after merging
|
||||
--clip = Only pick trims that are using this clip name. Ex: ClipX.Trim(0,1) or Trim(ClipX,0,1)
|
||||
--uid = Set base UID for --template/--chnames
|
||||
--chnames = Path to basic text containing chapter titles separated by newlines
|
||||
--ofps = Output FPS (used in qpfile, v2 timecodes and avs export)
|
||||
Default: -f
|
||||
--timecodes = Output v2 timecodes (from fps and v1 parsing) (if using --ofps, outputs v2 timecodes using this)
|
||||
--sbr = Set this if inputting an .aac and it's SBR/HE-AAC
|
||||
--test = Test Mode (doesn't create new files)
|
||||
outtrims.avs = If chapparse.py is present, outputs .avs with offset and converted trims
|
||||
|
||||
To do:
|
||||
* Optimize code and/or improve its legibility
|
||||
|
||||
Known issues:
|
||||
* Conversion from a different input fps to output fps is not accurate (probably no way it can ever be fixed)
|
263
bin/vfr/chapparse.py
Normal file
263
bin/vfr/chapparse.py
Normal file
|
@ -0,0 +1,263 @@
|
|||
#!/usr/bin/env python
|
||||
# chapparse.py
|
||||
import sys, re, getopt, os
|
||||
from string import Template
|
||||
|
||||
name = 'chapparse.py'
|
||||
version = '0.4'
|
||||
rat = re.compile('(\d+)(?:/|:)(\d+)')
|
||||
chapre = re.compile("CHAPTER\d+=(\S+)",re.I)
|
||||
x264 = 'x264-64'
|
||||
ffmpeg = 'ffmpeg'
|
||||
mkvmerge = 'mkvmerge'
|
||||
avs2yuv = 'avs2yuv'
|
||||
timeCodes = frameNumbers = merge = []
|
||||
|
||||
def main():
|
||||
try:
|
||||
opts, args = getopt.getopt(sys.argv[1:], "i:o:f:b:e:s:a:x:c:hmr",['help','avs=','test','x264opts='])
|
||||
except getopt.GetoptError as err:
|
||||
print(err)
|
||||
help()
|
||||
sys.exit()
|
||||
|
||||
set = dict(input='video.mkv',output='',audio='',index='',
|
||||
fps='24000/1001',batch='',method='x264',resize='',avs='',mergeFiles=False,removeFiles=False,
|
||||
x264opts='--preset placebo --crf 16 --level 41 --rc-lookahead 250',test=False,
|
||||
x264=x264,ffmpeg=ffmpeg,mkvmerge=mkvmerge,avs2yuv=avs2yuv,chapters='chapters.txt',crop='0,0,0,0')
|
||||
|
||||
for o, v in opts:
|
||||
if o == '-i':
|
||||
set['input'] = v
|
||||
elif o == '-o':
|
||||
set['output'] = v[:-4]
|
||||
elif o == '-f':
|
||||
set['fps'] = v
|
||||
elif o == '-b':
|
||||
set['batch'] = v
|
||||
elif o == '-e':
|
||||
set['method'] = v
|
||||
elif o == '-s':
|
||||
set['resize'] = v
|
||||
elif o == '-c':
|
||||
set['crop'] = v
|
||||
elif o in ('-x','--x264opts'):
|
||||
set['x264opts'] = v
|
||||
elif o == '-a':
|
||||
set['audio'] = v
|
||||
elif o in ('-h','--help'):
|
||||
help()
|
||||
sys.exit()
|
||||
elif o == '-m':
|
||||
set['mergeFiles'] = True
|
||||
elif o == '-r':
|
||||
set['removeFiles'] = True
|
||||
elif o == '--avs':
|
||||
set['avs'] = v
|
||||
elif o == '--test':
|
||||
set['test'] = True
|
||||
else:
|
||||
assert False, "unhandled option"
|
||||
|
||||
set['chapters'] = set['chapters'] if len(args) != 1 else args[0]
|
||||
|
||||
if set['output'] == '':
|
||||
set['output'] = set['input'][:-4]+'.encode'
|
||||
if set['avs'] == '' and set['method'] == 'avisynth':
|
||||
set['avs'] = set['output']+'.avs'
|
||||
if set['avs'] != '' and set['method'] == 'x264':
|
||||
set['method'] = 'avisynth'
|
||||
if set['batch'] == '':
|
||||
set['batch'] = set['output']+'.bat'
|
||||
if os.path.isfile(set['chapters']) != True:
|
||||
print("You must set a valid OGM chapters file.")
|
||||
sys.exit(2)
|
||||
|
||||
if set['test'] == True:
|
||||
for key in sorted(set):
|
||||
print(key.ljust(8),'=',set[key])
|
||||
print()
|
||||
|
||||
timeStrings = parseOgm(args[0])
|
||||
|
||||
timeCodes = [time2ms(timeString) for timeString in timeStrings]
|
||||
|
||||
frameNumbers = [ms2frame(timeCode,set['fps']) for timeCode in timeCodes]
|
||||
|
||||
set['cmd'] = Template('${piper}"${x264}" ${x264opts} --demuxer y4m${end} - -o "${output}-part${part}.mkv"')
|
||||
|
||||
if set['method'] == 'avisynth':
|
||||
set['avs'] = '"%s"' % set['avs']
|
||||
if set['test'] == False:
|
||||
set = writeAvisynth(set,frameNumbers)
|
||||
else:
|
||||
print('Writing avisynth script')
|
||||
elif set['method'] == 'ffmpeg':
|
||||
set['resize'] = ' -s '+set['resize'] if (set['method'] == 'ffmpeg' and set['resize'] != '') else ''
|
||||
elif set['method'] == 'x264':
|
||||
set['cmd'] = Template('"${x264}" ${x264opts}${seek}${end} $xinput -o "${output}-part${part}.mkv"')
|
||||
set['index'] = '"%s.x264.ffindex"' % set['input'] if set['input'][-3:] in ('mkv','mp4','wmv') else ''
|
||||
set['xinput'] = '"%s" --index %s' % (set['input'],set['index']) if set['index'] != '' else '"%s"' % set['input']
|
||||
x264crop = 'crop:'+set['crop'] if (set['method'] == 'x264' and set['crop'] != '0,0,0,0') else ''
|
||||
x264resize='resize:'+','.join(set['resize'].split('x')) if (set['method'] == 'x264' and set['resize'] != '') else ''
|
||||
sep = '/' if (x264crop != '' and x264resize != '') else ''
|
||||
set['x264opts'] = set['x264opts']+' --vf %s%s%s' % (x264crop,sep,x264resize) if (x264crop != '' or x264resize != '') else set['x264opts']
|
||||
|
||||
writeBatch(set,frameNumbers,timeStrings)
|
||||
|
||||
def help():
|
||||
print("""
|
||||
%s %s
|
||||
Usage: chapparse.py [options] chapters.txt
|
||||
chapters.txt is an OGM chapters file to get chapter points from whence to
|
||||
separate the encodes
|
||||
|
||||
Options:
|
||||
-i video.mkv
|
||||
Video to be encoded
|
||||
-o encode.mkv
|
||||
Encoded video
|
||||
-f 24000/1001
|
||||
Frames per second
|
||||
-s 1280x720
|
||||
Resolution to resize to (no default)
|
||||
-e x264
|
||||
Method of resizing [avisynth,ffmpeg,x264]
|
||||
-a audio.m4a
|
||||
Audio to mux in the final file
|
||||
-b encode.bat
|
||||
Batch file with the instructions for chapter-separated encode
|
||||
-x "--preset placebo --crf 16 --level 41 --rc-lookahead 250", --x264opts
|
||||
x264 options (don't use --demuxer, --input, --output or --frames)
|
||||
--avs encode.avs
|
||||
If using avisynth method
|
||||
-m
|
||||
Merge parts
|
||||
-r
|
||||
Remove extra files
|
||||
-h, --help
|
||||
This help file""" % (name,version))
|
||||
|
||||
def time2ms(ts):
|
||||
|
||||
t = ts.split(':')
|
||||
h = int(t[0]) * 3600000
|
||||
m = h + int(t[1]) * 60000
|
||||
ms = round(m + float(t[2]) * 1000)
|
||||
|
||||
return ms
|
||||
|
||||
def ms2frame(ms,fps):
|
||||
|
||||
s = ms / 1000
|
||||
fps = rat.search(fps).groups() if rat.search(fps) else \
|
||||
[re.search('(\d+)',fps).group(0),'1']
|
||||
frame = round((int(fps[0])/int(fps[1])) * s)
|
||||
|
||||
return frame
|
||||
|
||||
def parseOgm(file):
|
||||
|
||||
timeStrings = []
|
||||
|
||||
with open(file) as chapFile:
|
||||
for line in chapFile:
|
||||
timeString = chapre.match(line)
|
||||
if timeString != None:
|
||||
timeStrings.append(timeString.group(1))
|
||||
|
||||
return timeStrings
|
||||
|
||||
def writeAvisynth(set,frameNumbers):
|
||||
# needs dict with 'avs', 'input', 'resize' (if needed) and list with frameNumbers
|
||||
if os.path.isfile(set['avs'][1:-1]) != True:
|
||||
with open(set['avs'][1:-1],'w') as avs:
|
||||
if set['input'][:-4] in ('.mkv','.wmv','.mp4'):
|
||||
avs.write('FFVideoSource("%s")\n' % set['input'])
|
||||
elif set['input'][:-4] == '.avi':
|
||||
avs.write('AviSource("%s")\n' % set['input'])
|
||||
elif set['input'] != '':
|
||||
avs.write('DirectShowSource("%s")\n' % set['input'])
|
||||
if set['resize'] != '':
|
||||
avs.write('Spline36Resize(%s)\n' % ','.join(set['resize'].split('x')))
|
||||
avs.write('+'.join(['Trim(%d,%d)' % (frameNumbers[i],frameNumbers[i+1]-1) for i in range(len(frameNumbers)-1)]))
|
||||
avs.write('+Trim(%d,0)\n' % frameNumbers[-1])
|
||||
else:
|
||||
with open(set['avs'][1:-1],'a') as avs:
|
||||
avs.write('\n')
|
||||
avs.write('+'.join(['Trim(%d,%d)' % (frameNumbers[i],frameNumbers[i+1]-1) for i in range(len(frameNumbers)-1)]))
|
||||
avs.write('+Trim(%d,0)\n' % frameNumbers[-1])
|
||||
|
||||
set['resize'] = ''
|
||||
if set['input'][:-3] in ('mkv','wmv','mp4'):
|
||||
set['index'] = '"%s.mkv.ffindex"' % set['output']
|
||||
|
||||
return set
|
||||
|
||||
def cmdMake(set,frameNumbers,timeStrings,i):
|
||||
begin = frameNumbers[i]
|
||||
frames = frameNumbers[i+1]-begin if i != len(frameNumbers)-1 else 0
|
||||
|
||||
if set['method'] == 'avisynth':
|
||||
set['seek'] = ' -seek %d' % begin
|
||||
elif set['method'] == 'ffmpeg':
|
||||
set['seek'] = ' -ss %s' % timeStrings[i]
|
||||
elif set['method'] == 'x264':
|
||||
set['seek'] = ' --seek %d' % begin
|
||||
if frames != 0:
|
||||
if set['method'] == 'avisynth':
|
||||
set['frames'] = ' -frames %d' % frames
|
||||
elif set['method'] == 'ffmpeg':
|
||||
set['frames'] = ' -vframes %d' % frames
|
||||
elif set['method'] == 'x264':
|
||||
set['frames'] = ''
|
||||
set['end'] = ' --frames %d' % frames
|
||||
else:
|
||||
set['end'] = set['frames'] = ''
|
||||
|
||||
set['merge'] = '"%s-part%d.mkv"' % (set['output'],i+1)
|
||||
|
||||
set['part'] = i+1
|
||||
|
||||
if set['method'] == 'avisynth':
|
||||
set['piper'] = Template('"${avs2yuv}"${seek}${frames} $avs -o - | ')
|
||||
elif set['method'] == 'ffmpeg':
|
||||
set['piper'] = Template('"${ffmpeg}" -i "${input}"${resize}${seek}${frames} -f yuv4mpegpipe -sws_fags spline - | ')
|
||||
|
||||
if set['method'] in ('avisynth','ffmpeg'):
|
||||
set['piper'] = set['piper'].substitute(set)
|
||||
|
||||
return set
|
||||
|
||||
def writeBatch(set,frameNumbers,timeStrings):
|
||||
if set['test'] == False:
|
||||
with open(set['batch'],'w') as batch:
|
||||
merge = []
|
||||
if os.name == 'posix':
|
||||
batch.write('#!/bin/sh\n\n')
|
||||
for i in range(len(frameNumbers)):
|
||||
set2 = cmdMake(set,frameNumbers,timeStrings,i)
|
||||
batch.write(set['cmd'].substitute(set2)+'\n')
|
||||
merge.append(set2['merge'])
|
||||
|
||||
if set['mergeFiles'] == True:
|
||||
batch.write('\n"%s" -o "%s" %s --default-duration "1:%sfps"' % (set['mkvmerge'],set['output']+'.mkv',' +'.join(merge),set['fps']))
|
||||
if set['audio'] != '':
|
||||
batch.write(' -D --no-chapters "%s"' % set['audio'])
|
||||
batch.write(' --chapters "%s"' % set['chapters'])
|
||||
batch.write('\n')
|
||||
rem = ' '.join(merge)
|
||||
if set['removeFiles'] == True and os.name == 'nt':
|
||||
batch.write('del %s' % rem)
|
||||
elif set['removeFiles'] == True and os.name == 'posix':
|
||||
batch.write('rm %s' % rem)
|
||||
else:
|
||||
print('Writing batch file')
|
||||
#print('Example:',set['cmd'].format(cmdMake(set,frameNumbers,timeStrings,3)))
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) > 1:
|
||||
main()
|
||||
else:
|
||||
print('Usage: chapparse.py [options] chapters.txt')
|
||||
sys.exit()
|
0
bin/vfr/output.txt
Normal file
0
bin/vfr/output.txt
Normal file
16
bin/vfr/tcconv.py
Normal file
16
bin/vfr/tcconv.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
from sys import argv
|
||||
try:
|
||||
from vfr import parse_tc
|
||||
except ImportError:
|
||||
exit("tcconv requires vfr.py in order to work")
|
||||
|
||||
if len(argv) >= 4:
|
||||
fps = argv[1]
|
||||
tc = argv[2]
|
||||
frames = int(argv[3])
|
||||
first = int(argv[4]) if len(argv) == 5 else 0
|
||||
parse_tc(fps, frames, tc, first)
|
||||
else:
|
||||
exit("tcconv.py <fps/v1 timecodes> <output v2 timecodes> <frames> [<first>]")
|
353
bin/vfr/templates.py
Normal file
353
bin/vfr/templates.py
Normal file
|
@ -0,0 +1,353 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
from __future__ import unicode_literals
|
||||
from io import open
|
||||
|
||||
class AutoMKVChapters:
|
||||
class Template:
|
||||
def __init__(self):
|
||||
from random import randint
|
||||
self.uid = randint(10**4,10**6)
|
||||
self.num_editions = 1
|
||||
self.lang = ['eng']
|
||||
self.country = ['us']
|
||||
self.fps = '30'
|
||||
self.ofps = '24'
|
||||
self.qpf = '0'
|
||||
self.idr = False
|
||||
self.trims = None
|
||||
self.kframes = None
|
||||
|
||||
def toxml(self,chapfile):
|
||||
|
||||
chf = open(chapfile+'.xml','w',encoding='utf-8')
|
||||
head = '<?xml version="1.0" encoding="UTF-8"?>\n<!-- <!DOCTYPE Tags SYSTEM "matroskatags.dtd"> -->\n'
|
||||
chf.write(head+'<Chapters>\n')
|
||||
|
||||
if self.num_editions > 1:
|
||||
tagf = open(chapfile+'tags.xml','w')
|
||||
tagf.write(head+'<Tags>\n')
|
||||
else:
|
||||
tagf = False
|
||||
|
||||
for ed in self.editions:
|
||||
chf.write('\t<EditionEntry>\n')
|
||||
chf.write('\t\t<EditionFlagHidden>{0:d}</EditionFlagHidden>\n'.format(ed.hidden) if ed.hidden else '')
|
||||
chf.write('\t\t<EditionFlagDefault>{0:d}</EditionFlagDefault>\n'.format(ed.default) if ed.default else '')
|
||||
chf.write('\t\t<EditionFlagOrdered>{0:d}</EditionFlagOrdered>\n'.format(ed.ordered) if ed.ordered else '')
|
||||
chf.write('\t\t<EditionUID>{0:d}</EditionUID>\n'.format(ed.uid))
|
||||
|
||||
if tagf:
|
||||
tagf.write('\t<Tag>\n\t\t<Targets>\n')
|
||||
tagf.write('\t\t\t<EditionUID>{0:d}</EditionUID>\n'.format(ed.uid))
|
||||
tagf.write('\t\t\t<TargetTypeValue>50</TargetTypeValue>\n\t\t</Targets>\n')
|
||||
num_names = len(ed.name) if len(ed.name) < len(self.lang) else len(self.lang)
|
||||
for i in range(num_names):
|
||||
tagf.write('\t\t<Simple>\n\t\t\t<Name>TITLE</Name>\n')
|
||||
tagf.write('\t\t\t<String>{0}</String>\n'.format(ed.name[i] if ed.name[i] != '' else ed.name[i-1]))
|
||||
tagf.write('\t\t\t<TagLanguage>{0}</TagLanguage>\n'.format(self.lang[i]))
|
||||
tagf.write('\t\t\t<DefaultLanguage>{0:d}</DefaultLanguage>\n'.format(1 if i == 0 else 0))
|
||||
tagf.write('\t\t</Simple>\n')
|
||||
tagf.write('\t</Tag>\n')
|
||||
|
||||
for ch in ed.chapters:
|
||||
chf.write('\t\t<ChapterAtom>\n')
|
||||
num_names = len(ch.name) if len(ch.name) < len(self.lang) else len(self.lang)
|
||||
for i in range(num_names):
|
||||
chf.write('\t\t\t<ChapterDisplay>\n')
|
||||
chf.write('\t\t\t\t<ChapterString>{0}</ChapterString>\n'.format(ch.name[i] if ch.name[i] != '' else ch.name[i-1]))
|
||||
chf.write('\t\t\t\t<ChapterLanguage>{0}</ChapterLanguage>\n'.format(self.lang[i]) if self.lang[i] != 'eng' else '')
|
||||
chf.write('\t\t\t\t<ChapterCountry>{0}</ChapterCountry>\n'.format(self.country[i]) if i < len(self.country) else '')
|
||||
chf.write('\t\t\t</ChapterDisplay>\n')
|
||||
chf.write('\t\t\t<ChapterUID>{0:d}</ChapterUID>\n'.format(ch.uid))
|
||||
chf.write('\t\t\t<ChapterTimeStart>{0}</ChapterTimeStart>\n'.format(ch.start))
|
||||
chf.write('\t\t\t<ChapterTimeEnd>{0}</ChapterTimeEnd>\n'.format(ch.end) if ch.end else '')
|
||||
chf.write('\t\t\t<ChapterFlagHidden>{0:d}</ChapterFlagHidden>\n'.format(ch.hidden) if ch.hidden != 0 else '')
|
||||
chf.write('\t\t\t<ChapterFlagEnabled>{0:d}</ChapterFlagEnabled>\n'.format(ch.enabled) if ch.enabled != 1 else '')
|
||||
chf.write('\t\t\t<ChapterSegmentUID format="hex">{0}</ChapterSegmentUID>\n'.format(ch.suid) if ch.suid else '')
|
||||
chf.write('\t\t</ChapterAtom>\n')
|
||||
|
||||
chf.write('\t</EditionEntry>\n')
|
||||
|
||||
chf.write('</Chapters>\n')
|
||||
|
||||
if tagf:
|
||||
tagf.write('</Tags>\n')
|
||||
tagf.close()
|
||||
|
||||
if self.qpf != '0' and self.kframes:
|
||||
from vfr import write_qpfile
|
||||
if self.qpf != '1':
|
||||
qpfile = self.qpf
|
||||
else:
|
||||
qpfile = chapfile+'.qpfile'
|
||||
write_qpfile(qpfile, self.kframes, self.idr)
|
||||
|
||||
def connect_with_vfr(self,avs,label=None,clip=None):
|
||||
"""
|
||||
Connects templates.py with vfr.py, enabling its use outside of vfr.py.
|
||||
|
||||
Uses the same quirks as AMkvC but only for 24 and 30 fps.
|
||||
Ex: inputfps=30 is understood as being '30*1000/1001'
|
||||
|
||||
"""
|
||||
|
||||
from vfr import parse_trims, fmt_time
|
||||
|
||||
# compensate for amkvc's fps assumption
|
||||
if self.fps in ('24','30'):
|
||||
fps = self.fps + '/1.001'
|
||||
else:
|
||||
fps = str(self.fps)
|
||||
if self.ofps and self.ofps in ('24','30'):
|
||||
ofps = self.ofps + '/1.001'
|
||||
else:
|
||||
ofps = str(self.ofps)
|
||||
|
||||
Trims2, Trims2ts = parse_trims(avs, fps, ofps, label=label, clip=clip)[2:4]
|
||||
Trims2ts = [(fmt_time(i[0]),fmt_time(i[1]) if i[1] != 0 else None) for i in Trims2ts]
|
||||
|
||||
self.trims = Trims2ts
|
||||
self.kframes = Trims2
|
||||
|
||||
def parse_mkv(self, path):
|
||||
"""Parse a Matroska file for SegmentUID and Duration"""
|
||||
import binascii
|
||||
import struct
|
||||
|
||||
def get_data_len(byte):
|
||||
"""Get the length (bytes) of the element data"""
|
||||
n = ord(byte)
|
||||
mask = 0b10000000
|
||||
while not n & mask:
|
||||
mask >>= 1
|
||||
return n & ~mask
|
||||
|
||||
suid = tcscale = duration = 0
|
||||
with open(path, 'rb') as file:
|
||||
if file.read(4) != b'\x1A\x45\xDF\xA3': # not a Matroska file
|
||||
return suid, duration
|
||||
chunk_size = 100000 # 100 kB
|
||||
i = 0
|
||||
while True:
|
||||
if suid and tcscale and duration:
|
||||
break
|
||||
bin = file.read(chunk_size)
|
||||
if not bin:
|
||||
break
|
||||
suid_pos = bin.find(b'\x73\xA4\x90') # \x90 -> 16 bytes
|
||||
if suid_pos != -1:
|
||||
suid_pos = 4 + i * chunk_size + suid_pos + 3
|
||||
file.seek(suid_pos)
|
||||
suid = binascii.hexlify(file.read(16)).decode()
|
||||
tcscale_pos = bin.find(b'\x2A\xD7\xB1')
|
||||
if tcscale_pos != -1:
|
||||
tcscale_pos = 4 + i * chunk_size + tcscale_pos + 3
|
||||
file.seek(tcscale_pos)
|
||||
tcscale_len = get_data_len(file.read(1))
|
||||
tcscale = int(binascii.hexlify(file.read(tcscale_len)), 16)
|
||||
duration_pos = bin.find(b'\x44\x89\x84') # float (4 bytes)
|
||||
if duration_pos != -1:
|
||||
duration_pos = 4 + i * chunk_size + duration_pos + 3
|
||||
file.seek(duration_pos)
|
||||
duration = struct.unpack('>f', file.read(4))[0]
|
||||
if not duration: # double (8 bytes)
|
||||
duration_pos = bin.find(b'\x44\x89\x88')
|
||||
if duration_pos != -1:
|
||||
duration_pos = 4 + i * chunk_size + duration_pos + 3
|
||||
file.seek(duration_pos)
|
||||
duration = struct.unpack('>d', file.read(8))[0]
|
||||
if bin.find(b'\x1F\x43\xB6\x75') != -1:
|
||||
# segment info should be before the clusters
|
||||
break
|
||||
i += 1
|
||||
duration = duration * tcscale / 1000000
|
||||
return suid, duration
|
||||
|
||||
class Edition:
|
||||
def __init__(self):
|
||||
self.default = 0
|
||||
self.name = ['Default']
|
||||
self.hidden = 0
|
||||
self.ordered = 0
|
||||
self.num_chapters = 1
|
||||
self.uid = 0
|
||||
|
||||
class Chapter:
|
||||
def __init__(self):
|
||||
self.name = ['Chapter']
|
||||
self.chapter = False
|
||||
self.start = False
|
||||
self.end = False
|
||||
self.suid = False
|
||||
self.hidden = 0
|
||||
self.uid = 0
|
||||
self.enabled = 1
|
||||
|
||||
def __init__(self, templatefile, output=None, avs=None, trims=None,
|
||||
kframes=None, uid=None, label=None, ifps=None, clip=None,
|
||||
idr=False):
|
||||
try:
|
||||
import configparser
|
||||
except ImportError:
|
||||
import ConfigParser as configparser
|
||||
from io import open
|
||||
|
||||
# Init config
|
||||
config = configparser.ConfigParser()
|
||||
template = open(templatefile, encoding='utf-8')
|
||||
|
||||
# Read template
|
||||
config.readfp(template)
|
||||
template.close()
|
||||
|
||||
# Template defaults
|
||||
self = self.Template()
|
||||
self.editions = []
|
||||
self.uid = uid if uid else self.uid
|
||||
|
||||
# Set mkvinfo path
|
||||
from vfr import mkvmerge, parse_with_mkvmerge, fmt_time
|
||||
from os.path import dirname, join, isfile
|
||||
|
||||
# Set placeholder for mkvinfo output
|
||||
mkv_globbed = False
|
||||
mkvinfo = {}
|
||||
|
||||
for k, v in config.items('info'):
|
||||
if k == 'lang':
|
||||
self.lang = v.split(',')
|
||||
elif k == 'country':
|
||||
self.country = v.split(',')
|
||||
elif k == 'inputfps':
|
||||
self.fps = v
|
||||
elif k == 'outputfps':
|
||||
self.ofps = v
|
||||
elif k == 'createqpfile':
|
||||
self.qpf = v
|
||||
elif k == 'uid':
|
||||
self.uid = int(v)
|
||||
elif k == 'editions':
|
||||
self.num_editions = int(v)
|
||||
|
||||
if avs and not ifps:
|
||||
self.connect_with_vfr(avs, label, clip)
|
||||
elif trims:
|
||||
self.trims = trims
|
||||
self.kframes = kframes
|
||||
else:
|
||||
self.trims = False
|
||||
self.idr = idr
|
||||
|
||||
for i in range(self.num_editions):
|
||||
from re import compile
|
||||
ed = self.Edition()
|
||||
ed.uid = self.uid * 100
|
||||
self.uid += 1
|
||||
cuid = ed.uid
|
||||
ed.num = i+1
|
||||
ed.chapters = []
|
||||
stuff = {}
|
||||
|
||||
for k, v in config.items('edition{0:d}'.format(ed.num)):
|
||||
if k == 'default':
|
||||
ed.default = int(v)
|
||||
elif k == 'name':
|
||||
ed.name = v.split(',')
|
||||
elif k == 'ordered':
|
||||
ed.ordered = int(v)
|
||||
elif k == 'hidden':
|
||||
ed.hidden = int(v)
|
||||
elif k == 'chapters':
|
||||
ed.num_chapters = int(v)
|
||||
for i in range(ed.num_chapters):
|
||||
stuff[i+1] = []
|
||||
elif k == 'uid':
|
||||
ed.uid = int(v)
|
||||
else:
|
||||
opt_re = compile('(\d+)(\w+)')
|
||||
ret = opt_re.search(k)
|
||||
if ret:
|
||||
stuff[int(ret.group(1))].append((ret.group(2),v))
|
||||
|
||||
for j in range(ed.num_chapters):
|
||||
ch = self.Chapter()
|
||||
cuid += 1
|
||||
ch.uid = cuid
|
||||
ch.num = j+1
|
||||
|
||||
for k, v in stuff[j+1]:
|
||||
if k == 'name':
|
||||
ch.name = v.split(',')
|
||||
elif k == 'chapter':
|
||||
ch.chapter = int(v)
|
||||
elif k == 'start':
|
||||
ch.start = v
|
||||
elif k == 'end':
|
||||
ch.end = v
|
||||
elif k == 'suid':
|
||||
ch.suid = v.strip() if ret else 0
|
||||
elif k == 'hidden':
|
||||
ch.hidden = int(v)
|
||||
elif k == 'enabled':
|
||||
ch.enabled = int(v)
|
||||
|
||||
if ch.suid and not isfile(ch.suid):
|
||||
ch.suid = ch.suid.replace('0x','').lower().replace(' ','')
|
||||
|
||||
if ch.chapter and not (ch.start and ch.end):
|
||||
ch.start, ch.end = self.trims[ch.chapter-1] if self.trims else (ch.start, ch.end)
|
||||
elif ch.suid:
|
||||
mkvfiles = []
|
||||
if isfile(ch.suid):
|
||||
mkvfiles = [ch.suid]
|
||||
elif not mkv_globbed:
|
||||
from glob import glob
|
||||
mkvfiles = glob('*.mkv') + glob(join(dirname(avs),'*.mkv'))
|
||||
mkv_globbed = True
|
||||
if mkvfiles:
|
||||
if parse_with_mkvmerge:
|
||||
from subprocess import check_output
|
||||
import json
|
||||
for file in mkvfiles:
|
||||
info = check_output([mkvmerge, '-i', '-F', 'json',
|
||||
'--output-charset', 'utf-8', file]).decode('utf-8')
|
||||
try:
|
||||
props = json.loads(info).get("container", {}).get("properties", {})
|
||||
ch.suid = props.get("segment_uid", 0)
|
||||
duration = props.get("duration", 0)
|
||||
mkvinfo[ch.suid] = {'file': file,
|
||||
'duration': fmt_time(duration * 10**6)
|
||||
if duration else 0}
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
for file in mkvfiles:
|
||||
ch.suid, duration = self.parse_mkv(file)
|
||||
mkvinfo[ch.suid] = {'file': file,
|
||||
'duration': fmt_time(duration * 10**6)
|
||||
if duration else 0}
|
||||
if not (ch.start or ch.end):
|
||||
ch.start = fmt_time(0) if not ch.start else ch.start
|
||||
ch.end = mkvinfo[ch.suid]['duration'] if not ch.end and (ch.suid in mkvinfo) else ch.end
|
||||
|
||||
ed.chapters.append(ch)
|
||||
self.editions.append(ed)
|
||||
if output:
|
||||
self.toxml(output)
|
||||
|
||||
|
||||
def main(args):
|
||||
|
||||
template = args[0]
|
||||
output = args[1]
|
||||
avs = args[2] if len(args) == 3 else None
|
||||
|
||||
chaps = AutoMKVChapters(template,output,avs)
|
||||
|
||||
if __name__ == '__main__':
|
||||
from sys import argv, exit
|
||||
if len(argv) > 1:
|
||||
main(argv[1:])
|
||||
else:
|
||||
exit("templates.py <template file> <output filenames w/o extension> [<avisynth file>]")
|
728
bin/vfr/vfr.py
Normal file
728
bin/vfr/vfr.py
Normal file
|
@ -0,0 +1,728 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import unicode_literals
|
||||
from sys import exit, argv
|
||||
from re import compile
|
||||
from os.path import isfile, splitext
|
||||
from math import floor, ceil
|
||||
from fractions import Fraction
|
||||
from io import open
|
||||
|
||||
exts = {
|
||||
"xml": "MKV",
|
||||
"x264.txt": "X264"
|
||||
}
|
||||
default_fps = "30000/1001"
|
||||
|
||||
# Change the paths here if the programs aren't in your $PATH
|
||||
mkvmerge = r'mkvmerge'
|
||||
|
||||
# Check to utilize mkvtoolnix for obtaining the uid and duration of the mkv
|
||||
# files specified on templates, instead of letting this script parse them
|
||||
# directly (faster). Just in case the later fails.
|
||||
parse_with_mkvmerge = False
|
||||
|
||||
def main(args):
|
||||
from optparse import OptionParser
|
||||
p = OptionParser(description='Grabs avisynth trims and outputs chapter '
|
||||
'file, qpfile and/or cuts audio (works with cfr and '
|
||||
'vfr input)',
|
||||
version='VFR Chapter Creator 0.10.0',
|
||||
usage='%prog [options] infile.avs [outfile.avs]')
|
||||
p.add_option('--label', '-l', action="store", dest="label",
|
||||
help="Look for a trim() statement or succeeding comment only "
|
||||
"on lines matching LABEL. Default: case insensitive trim")
|
||||
p.add_option('--clip', action="store", dest="clip",
|
||||
help="Look for trims() using specific clip, like"
|
||||
"Trim(ClipX,0,100). Default: any trim")
|
||||
p.add_option('--line', '-g', action="store", type="int", dest="line",
|
||||
help="Specify directly the line used")
|
||||
p.add_option('--input', '-i', action="store", help='Audio file to be cut',
|
||||
dest="input")
|
||||
p.add_option('--output', '-o', action="store",
|
||||
help='Cut audio from MKVMerge', dest="output")
|
||||
p.add_option('--fps', '-f', action="store",
|
||||
help='Frames per second or Timecodes file', dest="fps")
|
||||
p.add_option('--ofps', action="store", help='Output frames per second',
|
||||
dest="ofps")
|
||||
p.add_option('--timecodes', action="store", help='Output v2 timecodes',
|
||||
dest="otc")
|
||||
p.add_option('--chapters', '-c', action="store",
|
||||
help='Chapters file [.{0}/.txt]'.format("/.".join(
|
||||
exts.keys())), dest="chapters")
|
||||
p.add_option('--chnames', '-n', action="store",
|
||||
help='Path to template file for chapter names (utf8 w/o bom)',
|
||||
dest="chnames")
|
||||
p.add_option('--template', '-t', action="store",
|
||||
help="Template file for chapters", dest="template")
|
||||
p.add_option('--uid', action="store",
|
||||
help="Base UID for --template or --chnames", dest="uid")
|
||||
p.add_option('--qpfile', '-q', action="store", help='QPFile for x264',
|
||||
dest="qpfile")
|
||||
p.add_option('--verbose', '-v', action="store_true", help='Verbose',
|
||||
dest="verbose")
|
||||
p.add_option('--merge', '-m', action="store_true", help='Merge cut files',
|
||||
dest="merge")
|
||||
p.add_option('--remove', '-r', action="store_true",
|
||||
help='Remove cut files', dest="remove")
|
||||
p.add_option('--delay', '-d', action="store",
|
||||
help="Set delay of audio (can be negative)", dest="delay")
|
||||
p.add_option('--reverse', '-b', action="store_true",
|
||||
help="Reverse parsing of .avs", dest="reverse")
|
||||
p.add_option('--test', action="store_true",
|
||||
help="Test mode (do not create new files)", dest="test")
|
||||
p.add_option('--IDR', '--idr', action="store_true",
|
||||
help="Set this to make qpfile with IDR frames instead of K frames",
|
||||
dest="IDR")
|
||||
p.add_option('--sbr', action="store_true",
|
||||
help="Set this if inputting an .aac and it's SBR/HE-AAC",
|
||||
dest="sbr")
|
||||
(o, a) = p.parse_args(args)
|
||||
|
||||
if len(a) < 1:
|
||||
p.error("No avisynth script specified.")
|
||||
if not o.fps:
|
||||
o.fps = default_fps
|
||||
ifps = False
|
||||
else:
|
||||
ifps = True
|
||||
|
||||
#Determine chapter type
|
||||
if o.chapters:
|
||||
chre = compile("\.({0})$(?i)".format("|".join(exts.keys())))
|
||||
ret = chre.search(o.chapters)
|
||||
chapter_type = exts[ret.group(1).lower()] if ret else "OGM"
|
||||
else:
|
||||
chapter_type = ''
|
||||
|
||||
if o.template and o.chnames:
|
||||
p.error("Choose either --chnames or --template, not both.")
|
||||
elif o.template and chapter_type != 'MKV':
|
||||
p.error("--template needs to output to .xml.")
|
||||
|
||||
if not o.output and o.input:
|
||||
ret = splitext(o.input)
|
||||
o.output = '{0}.cut.mka'.format(ret[0])
|
||||
|
||||
if o.verbose:
|
||||
status = "Avisynth file: \t{0}\n".format(a[0])
|
||||
status += "Label: \t\t{0}\n".format(o.label) if o.label else ""
|
||||
status += "Clip name: \t{0}\n".format(o.clip) if o.clip else ""
|
||||
status += ("Parsing order: \t{0}\n".format("Bottom to top" if
|
||||
o.reverse else "Top to bottom"))
|
||||
status += "Line: \t\t{0}\n".format(o.line) if o.line else ""
|
||||
status += ("Audio file: \t{0}{1}\n".format(o.input, "(SBR)" if o.sbr
|
||||
else "") if o.input else "")
|
||||
status += "Cut Audio file: {0}\n".format(o.output) if o.output else ""
|
||||
status += "Timecodes/FPS: \t{0}{1}\n".format(o.fps, " to " + o.ofps if
|
||||
o.ofps else "") if o.ofps != o.fps else ""
|
||||
status += "Output v2 Tc: \t{0}\n".format(o.otc) if o.otc else ""
|
||||
status += ("Chapters file: \t{0}{1}\n".format(o.chapters,
|
||||
" ({0})".format(chapter_type) if chapter_type else "") if
|
||||
o.chapters else "")
|
||||
status += ("Chapter Names: \t{0}\n".format(o.chnames) if o.chnames
|
||||
else "")
|
||||
status += ("Template file: \t{0}\n".format(o.template) if o.template
|
||||
else "")
|
||||
status += "QP file: \t{0} ({1} frames)\n".format(o.qpfile, 'I' if
|
||||
o.IDR else 'K') if o.qpfile else ""
|
||||
status += "\n"
|
||||
status += ("Merge/Rem files:{0}/{1}\n".format(o.merge, o.remove) if
|
||||
o.merge or o.remove else "")
|
||||
status += ("Verbose: \t{0}\n".format(o.verbose) if o.verbose
|
||||
else "")
|
||||
status += "Test Mode: \t{0}\n".format(o.test) if o.test else ""
|
||||
|
||||
print(status)
|
||||
|
||||
# Get frame numbers and corresponding timecodes from avs
|
||||
Trims, Trimsts, Trims2, Trims2ts, audio = parse_trims(a[0], o.fps, o.ofps,
|
||||
o.otc if not o.test else '', o.input,
|
||||
o.label, o.reverse, o.line, o.clip,
|
||||
o.merge)
|
||||
|
||||
nt2 = len(Trims2ts)
|
||||
if o.verbose:
|
||||
print('In trims: {0}\n'.format(', '.join(['({0},{1})'.format(i[0],
|
||||
i[1]) for i in Trims])))
|
||||
print('In timecodes: {0}\n'.format(', '.join(['({0},{1})'.format(i[0],
|
||||
i[1]) for i in Trimsts])))
|
||||
print('Out trims: {0}\n'.format(', '.join(['({0},{1})'.format(i[0],
|
||||
i[1]) for i in Trims2])))
|
||||
print('Out timecodes: {0}\n'.format(', '.join(['({0},{1})'.format(
|
||||
fmt_time(i[0]), fmt_time(i[1])) for i in Trims2ts])))
|
||||
|
||||
# make qpfile
|
||||
if o.qpfile and not o.template:
|
||||
if not o.test:
|
||||
write_qpfile(o.qpfile, Trims2, o.IDR)
|
||||
if o.verbose:
|
||||
print('Writing keyframes to {0}\n'.format(o.qpfile))
|
||||
|
||||
# make audio cuts
|
||||
if o.input:
|
||||
split_audio(audio, o.input, o.output, o.delay, o.sbr, o.merge, o.remove,
|
||||
o.verbose, o.test)
|
||||
|
||||
# make offseted avs
|
||||
if len(a) > 1:
|
||||
try:
|
||||
from chapparse import writeAvisynth
|
||||
fNum = [i[0] for i in Trims2]
|
||||
set = {'avs': '"' + a[1] + '"', 'input': '', 'resize': ''}
|
||||
writeAvisynth(set, fNum)
|
||||
except ImportError:
|
||||
print('Script chapparse.py needed for avisynth output to work.')
|
||||
|
||||
# write chapters
|
||||
if chapter_type:
|
||||
|
||||
if chapter_type == 'MKV':
|
||||
Trims2ts = [(fmt_time(i[0]), fmt_time(i[1]) if i[1] != 0 else None)
|
||||
for i in Trims2ts]
|
||||
|
||||
if o.template:
|
||||
from templates import AutoMKVChapters as amkvc
|
||||
output = o.chapters[:-4] if not o.test else None
|
||||
chaps = amkvc(o.template, output=output, avs=a[0], trims=Trims2ts,
|
||||
kframes=Trims2, uid=o.uid, label=o.label,
|
||||
ifps=ifps, clip=o.clip, idr=o.IDR)
|
||||
|
||||
else:
|
||||
# Assign names to each chapter if --chnames
|
||||
chapter_names = []
|
||||
|
||||
if o.chnames:
|
||||
with open(o.chnames, encoding='utf-8') as f:
|
||||
[chapter_names.append(line.strip()) for line in
|
||||
f.readlines()]
|
||||
|
||||
if not o.chnames or len(chapter_names) < len(Trims2ts):
|
||||
# The if statement is for clarity; it doesn't actually do
|
||||
# anything useful
|
||||
for i in range(len(chapter_names), len(Trims2ts)):
|
||||
chapter_names.append("Chapter {:02d}".format(i + 1))
|
||||
|
||||
if chapter_type == 'MKV':
|
||||
from templates import AutoMKVChapters as amkvc
|
||||
tmp = amkvc.Template()
|
||||
tmp.trims = Trims2ts
|
||||
tmp.kframes = Trims2
|
||||
if o.qpfile:
|
||||
tmp.qpf = o.qpfile
|
||||
tmp.idr = o.IDR
|
||||
ed = tmp.Edition()
|
||||
ed.default = 1
|
||||
ed.num_chapters = len(chapter_names)
|
||||
ed.uid = int(o.uid) * 100 if o.uid else tmp.uid * 100
|
||||
cuid = ed.uid
|
||||
ed.chapters = []
|
||||
for i in range(len(chapter_names)):
|
||||
ch = tmp.Chapter()
|
||||
cuid += 1
|
||||
ch.uid = cuid
|
||||
ch.name = [chapter_names[i]]
|
||||
ch.start, ch.end = (Trims2ts[i][0], Trims2ts[i][1])
|
||||
ed.chapters.append(ch)
|
||||
tmp.editions = [ed]
|
||||
chaps = tmp
|
||||
|
||||
if not o.test:
|
||||
if chapter_type == 'MKV':
|
||||
chaps.toxml(o.chapters[:-4])
|
||||
else:
|
||||
with open(o.chapters, "w", encoding='utf-8') as output:
|
||||
if chapter_type == 'OGM':
|
||||
chap = ('CHAPTER{1:02d}={0}\nCHAPTER{1:02d}'
|
||||
'NAME={2}\n')
|
||||
elif chapter_type == 'X264':
|
||||
chap = '{0} {2}\n'
|
||||
Trims2ts = [fmt_time(i[0], 1) for i in Trims2ts]
|
||||
[output.write(chap.format(Trims2ts[i], i + 1,
|
||||
chapter_names[i])) for i in range(len(Trims2ts))]
|
||||
if o.verbose:
|
||||
print("Writing {} Chapters to {}". format(chapter_type,
|
||||
o.chapters))
|
||||
|
||||
|
||||
def fmt_time(ts, msp=False):
|
||||
"""Converts nanosecond timestamps to timecodes.
|
||||
|
||||
msp = Set timecodes for millisecond precision if True
|
||||
|
||||
"""
|
||||
s = ts / 10 ** 9
|
||||
m = s // 60
|
||||
s = s % 60
|
||||
h = m // 60
|
||||
m = m % 60
|
||||
if msp:
|
||||
return '{:02.0f}:{:02.0f}:{:06.3f}'.format(h, m, s)
|
||||
else:
|
||||
return '{:02.0f}:{:02.0f}:{:012.9f}'.format(h, m, s)
|
||||
|
||||
|
||||
def truncate(ts, scale=0):
|
||||
"""Truncates a ns timestamp to 0.1*scale precision
|
||||
with an extra decimal place if it rounds up.
|
||||
|
||||
Default: 0 (0.1 ms)
|
||||
|
||||
Examples: 3 (0.1 µs); 6 (0.1 ns)
|
||||
|
||||
"""
|
||||
scale = abs(6 - scale)
|
||||
ots = ts / 10 ** scale
|
||||
tts = (floor(ots * 10) * 10 if round(ots, 1) == floor(ots * 10) / 10 else
|
||||
ceil(ots * 10) * 10 - 5)
|
||||
return int(tts * 10 ** (scale - 2))
|
||||
|
||||
|
||||
def correct_to_ntsc(fps, ms=False):
|
||||
"""Rounds framerate to NTSC values if close enough.
|
||||
Takes and returns a Rational number.
|
||||
|
||||
Ported from FFMS2.
|
||||
"""
|
||||
fps = Fraction(fps).limit_denominator(10**6)
|
||||
fps_list = (24, 25, 30, 48, 50, 60, 100, 120)
|
||||
|
||||
for fps_idx in fps_list:
|
||||
delta = (fps_idx - fps_idx / 1.001) / 2.0
|
||||
if abs(fps - fps_idx) < delta:
|
||||
fps = Fraction(fps_idx, 1)
|
||||
break
|
||||
elif (fps_idx % 25) and (abs(fps - fps_idx / 1.001) < delta):
|
||||
fps = Fraction(fps_idx * 1000, 1001)
|
||||
break
|
||||
|
||||
if not ms:
|
||||
return fps
|
||||
else:
|
||||
return float(1000 / fps)
|
||||
|
||||
|
||||
def convert_v1_to_v2(v1, max, asm, v2=None, first=0):
|
||||
"""Converts a given v1 timecodes file to v2 timecodes.
|
||||
|
||||
Original idea from tritical's tcConv.
|
||||
|
||||
"""
|
||||
ts = fn1 = fn2 = last = 0
|
||||
asm = correct_to_ntsc(asm, True)
|
||||
o = []
|
||||
ap = o.append
|
||||
en = str.encode
|
||||
for line in v1:
|
||||
ovr = line.split(',')
|
||||
if len(ovr) == 3:
|
||||
fn1, fn2, fps = ovr
|
||||
fn1 = int(fn1)
|
||||
fn2 = int(fn2)
|
||||
ovf = correct_to_ntsc(fps, True)
|
||||
while (last < fn1 and last < max):
|
||||
ap(ts)
|
||||
last, ts = last + 1, ts + asm
|
||||
while (last <= fn2 and last < max):
|
||||
ap(ts)
|
||||
last, ts = last + 1, ts + ovf
|
||||
while last < max:
|
||||
ap(ts)
|
||||
last, ts = last + 1, ts + asm
|
||||
if v2:
|
||||
with open(v2, 'wb') as v2f:
|
||||
from os import linesep as ls
|
||||
header = [en('# timecode format v2' + ls)] if first == 0 else [b'']
|
||||
v2f.writelines(header + [en(('{0:3.6f}'.format(s)) + ls) for s in
|
||||
o[first:]])
|
||||
return o[first:]
|
||||
|
||||
|
||||
def parse_tc(tcfile, max=0, otc=None, first=0):
|
||||
"""Parses a timecodes file or cfr fps.
|
||||
|
||||
tcfile = timecodes file or cfr fps to parse
|
||||
max = number of frames to be created in v1 parsing
|
||||
otc = output v2 timecodes filename
|
||||
|
||||
"""
|
||||
|
||||
cfr_re = compile('(\d+(?:\.\d+)?)(?:/|:)?(\d+(?:\.\d+)?)?')
|
||||
vfr_re = compile('# time(?:code|stamp) format (v1|v2)')
|
||||
|
||||
ret = cfr_re.search(tcfile)
|
||||
if ret and not isfile(tcfile):
|
||||
type = 'cfr'
|
||||
num = Fraction(ret.group(1))
|
||||
den = Fraction(ret.group(2)) if ret.group(2) else 1
|
||||
timecodes = Fraction(num, den)
|
||||
if otc:
|
||||
convert_v1_to_v2([], max + 2, timecodes, otc, first)
|
||||
|
||||
else:
|
||||
type = 'vfr'
|
||||
with open(tcfile) as tc:
|
||||
v1 = tc.readlines()
|
||||
ret = vfr_re.search(v1.pop(0))
|
||||
version = ret.group(1) if ret else exit('File is not in a supported '
|
||||
'format.')
|
||||
|
||||
if version == 'v1':
|
||||
ret = v1.pop(0).split(' ')
|
||||
asm = ret[1] if len(ret) == 2 else exit('there is no assumed fps')
|
||||
if v1:
|
||||
ret = convert_v1_to_v2(v1, max, asm, otc, first)
|
||||
timecodes = ['{0:3.6f}\n'.format(i) for i in ret]
|
||||
else:
|
||||
timecodes = correct_to_ntsc(asm)
|
||||
type = 'cfr'
|
||||
if otc:
|
||||
convert_v1_to_v2([], max + 2, timecodes, otc, first)
|
||||
|
||||
elif version == 'v2':
|
||||
if max > len(v1):
|
||||
temp_max = len(v1)
|
||||
sample = temp_max // 100
|
||||
average = 0
|
||||
for i in range(-sample, 0):
|
||||
average += round(float(v1[i]) - float(v1[i - 1]), 6)
|
||||
fps = correct_to_ntsc(Fraction.from_float(sample / average *
|
||||
1000))
|
||||
ret = convert_v1_to_v2([], max - len(v1) + 1, fps, first=1)
|
||||
if v1[-1][-1] is not '\n':
|
||||
v1[-1] += '\n'
|
||||
v1 += ['{0:3.6f}\n'.format(i + float(v1[-1])) for i in ret]
|
||||
timecodes = v1
|
||||
|
||||
return (timecodes, type), max
|
||||
|
||||
|
||||
def get_ts(fn, tc, scale=0):
|
||||
"""Returns timestamps from a frame number and timecodes file or cfr fps
|
||||
|
||||
fn = frame number
|
||||
tc = (timecodes list or Fraction(fps),tc_type)
|
||||
|
||||
scale default: 0 (ns)
|
||||
examples: 3 (µs); 6 (ms); 9 (s)
|
||||
|
||||
"""
|
||||
scale = 9 - scale
|
||||
tc, tc_type = tc
|
||||
if tc_type == 'cfr':
|
||||
ts = round(10 ** scale * fn * Fraction(tc.denominator, tc.numerator))
|
||||
return ts
|
||||
elif tc_type == 'vfr':
|
||||
ts = round(float(tc[fn]) * 10 ** (scale - 3))
|
||||
return ts
|
||||
|
||||
|
||||
def convert_fps(ofn, old, new, oldts=None):
|
||||
"""Returns a frame number from fps and ofps (ConvertFPS)
|
||||
|
||||
fn = frame number
|
||||
old = original fps ('30000/1001', '25')
|
||||
new = output fps ('24000/1001', etc.)
|
||||
|
||||
"""
|
||||
|
||||
newts = 0
|
||||
nfn = 0
|
||||
thr = get_ts(1, old) # milliseconds
|
||||
newframes = []
|
||||
newtimestamps = []
|
||||
temp = temp2 = []
|
||||
|
||||
for i in ofn:
|
||||
for j in i:
|
||||
temp.append(j)
|
||||
|
||||
ofn = temp
|
||||
|
||||
if not oldts:
|
||||
oldtsi = []
|
||||
for fn in ofn:
|
||||
oldtsi.append(get_ts(fn, old))
|
||||
else:
|
||||
oldtsi = []
|
||||
for i in oldts:
|
||||
for j in i:
|
||||
oldtsi.append(j)
|
||||
|
||||
for i in range(len(ofn)):
|
||||
|
||||
fn = ofn[i]
|
||||
ots = oldtsi[i]
|
||||
nts = get_ts(nfn, new)
|
||||
if ots - nts >= thr:
|
||||
while (ots - nts > thr):
|
||||
nfn += 1
|
||||
nts = get_ts(nfn, new)
|
||||
if len(newframes) != 0 and nfn == newframes[-1]:
|
||||
newframes[-1] -= 1
|
||||
newtimestamps[-1] = get_ts(newframes[-1], new)
|
||||
newframes.append(nfn)
|
||||
newtimestamps.append(nts)
|
||||
else:
|
||||
newframes.append(nfn)
|
||||
newtimestamps.append(nts)
|
||||
elif ots - nts < thr:
|
||||
if len(newframes) != 0 and nfn == newframes[-1]:
|
||||
newframes[-1] -= 1
|
||||
newframes.append(nfn)
|
||||
newtimestamps.append(nts)
|
||||
else:
|
||||
newframes.append(nfn)
|
||||
newtimestamps.append(nts)
|
||||
else:
|
||||
nfn = 0
|
||||
while (ots - nts > thr):
|
||||
nfn += 1
|
||||
nts = get_ts(nfn, new)
|
||||
newframes.append(nfn)
|
||||
newtimestamps.append(nts)
|
||||
|
||||
if len(newframes) % 2 == 0:
|
||||
temp = []
|
||||
temp2 = []
|
||||
for i in range(0, len(newframes), 2):
|
||||
temp.append([newframes[i], newframes[i + 1]])
|
||||
temp2.append([newtimestamps[i], newtimestamps[i + 1]])
|
||||
newframes, newtimestamps = temp, temp2
|
||||
|
||||
if oldts:
|
||||
return newframes, newtimestamps
|
||||
else:
|
||||
return newframes
|
||||
|
||||
|
||||
def parse_avs(avs, label=None, reverse=None, line_number=None, clip=None):
|
||||
"""Parse an avisynth file. Scours it for the first uncommented trim line.
|
||||
|
||||
By default it looks for case-insensitive 'trim'. Using label, you can make
|
||||
it parse only the line starting with a certain case of trim, ignoring the
|
||||
others. Ex: label = 'tRiM' looks for the line starting with tRiM, ignoring
|
||||
other cases.
|
||||
|
||||
Returns a list with pairs of frames containing the first and last of each
|
||||
trim.
|
||||
|
||||
"""
|
||||
|
||||
Trims = []
|
||||
|
||||
trim_label = 'trim'
|
||||
comment = ''
|
||||
ignore_case = '(?i)'
|
||||
trim_clip = ''
|
||||
|
||||
if line_number:
|
||||
label = None
|
||||
if label and label.lower() == 'trim':
|
||||
trim_label = label
|
||||
comment = ''
|
||||
ignore_case = ''
|
||||
elif label:
|
||||
comment = '#.*' + label
|
||||
if clip:
|
||||
trimre = compile('(?<!#)(?:{0}\.trim\(|trim\({0}\s*,\s*)(\d+)\s*,\s*(-?\d+)\)(?i)'.format(clip))
|
||||
findTrims = compile("(?<!#)[^#]*(?:{1}\.{0}\(|{0}\({1}\s*,\s*)(\d+)\s*,"
|
||||
"\s*(-?\d+)\).*{2}{3}".format(trim_label, clip, comment, ignore_case))
|
||||
else:
|
||||
trimre = compile('(?<!#)(?:\w+\.)?trim\((?:\w+\s*,\s*)?(\d+)\s*,\s*(-?\d+)\)(?i)'.format(clip))
|
||||
findTrims = compile("(?<!#)[^#]*\s*\.?\s*{0}\((?:\w+\s*,\s*)?(\d+)\s*,"
|
||||
"\s*(-?\d+)\).*{1}{2}".format(trim_label, comment, ignore_case))
|
||||
|
||||
with open(avs) as avsfile:
|
||||
avs = avsfile.readlines()
|
||||
if line_number:
|
||||
avs = avs[line_number - 1:line_number]
|
||||
for line in avs if not reverse else reversed(avs):
|
||||
if findTrims.match(line):
|
||||
Trims = trimre.findall(line)
|
||||
break
|
||||
if not Trims:
|
||||
if label:
|
||||
exit("Error: Avisynth script has no uncommented trims with label "
|
||||
"'{}'".format(label))
|
||||
if line_number:
|
||||
exit("Error: Avisynth script has no uncommented trims on line {}"
|
||||
.format(line_number))
|
||||
if clip:
|
||||
exit("Error: Avisynth script has no uncommented trims with clip "
|
||||
"'{}'".format(clip))
|
||||
exit("Error: Avisynth script has no uncommented trims")
|
||||
|
||||
return Trims
|
||||
|
||||
|
||||
def parse_trims(avs, fps, outfps=None, otc=None, input=None, label=None,
|
||||
reverse=None, line_number=None, clip=None, merge=True):
|
||||
"""Parse trims from an avisynth file.
|
||||
|
||||
Returns 5 lists containing:
|
||||
Trims = Trims as parsed from the avs without processing.
|
||||
Trimsts = Timecodes of each trim in Trims.
|
||||
Trims2 = Offset trims. If ofps is set, they will be using ofps's frame
|
||||
numbers.
|
||||
Trims2ts = Same as Trimsts only for Trims2.
|
||||
audio = Timecodes where each cut will be performed for audio.
|
||||
If the cuts are for adjacent frames they won't be appended to the
|
||||
list, so as to avoid unnecessary cuts.
|
||||
Ex: trim(0,10)+trim(11,20) will be output as trim(0,20)
|
||||
|
||||
"""
|
||||
|
||||
Trims = parse_avs(avs, label, reverse, line_number, clip)
|
||||
audio = []
|
||||
Trimsts = []
|
||||
Trims2 = []
|
||||
Trims2ts = []
|
||||
nt1 = len(Trims)
|
||||
adjacent = False
|
||||
|
||||
# Parse timecodes/fps
|
||||
last_frame = int(Trims[-1][1])
|
||||
if last_frame < 0:
|
||||
last_frame = int(Trims[-1][0]) - int(Trims[-1][1]) - 1
|
||||
elif last_frame == 0:
|
||||
last_frame = int(Trims[-1][0])
|
||||
|
||||
tc, max = parse_tc(fps, last_frame + 2, otc)
|
||||
if tc[1] == 'vfr' and outfps:
|
||||
exit("Can't use --ofps with timecodes file input")
|
||||
if outfps and fps != outfps:
|
||||
ofps = parse_tc(outfps, int(Trims[-1][1]) + 2)[0]
|
||||
if otc:
|
||||
max = convert_fps([[int(Trims[-1][1])]], tc, ofps)[0]
|
||||
parse_tc(outfps, max + 2, otc + '.ofps.txt')
|
||||
|
||||
# Parse trims
|
||||
for i in range(nt1):
|
||||
fn1 = int(Trims[i][0])
|
||||
fn1ts = get_ts(fn1, tc)
|
||||
fn1tsaud = get_ts(fn1, tc)
|
||||
fn2 = int(Trims[i][1])
|
||||
if fn2 > 0:
|
||||
fn2ts = get_ts(fn2 + 1, tc)
|
||||
fn2tsaud = get_ts(fn2 + 1, tc)
|
||||
adjacent = False
|
||||
Trimsts.append((fmt_time(fn1ts), fmt_time(fn2ts)))
|
||||
elif fn2 < 0:
|
||||
fn2 = fn1 - fn2 - 1
|
||||
fn2ts = get_ts(fn2 + 1, tc)
|
||||
fn2tsaud = get_ts(fn2 + 1, tc)
|
||||
adjacent = False
|
||||
Trimsts.append((fmt_time(fn1ts), fmt_time(fn2ts)))
|
||||
else:
|
||||
fn2ts = 0
|
||||
fn2tsaud = 0
|
||||
Trimsts.append((fmt_time(fn1ts), 0))
|
||||
|
||||
# Calculate offsets for non-continuous trims
|
||||
if i == 0:
|
||||
offset = 0
|
||||
offsetts = 0
|
||||
# If the first trim doesn't start at 0
|
||||
if fn1 > 0:
|
||||
offset = fn1
|
||||
offsetts = fn1ts
|
||||
else:
|
||||
# If it's not the first trim
|
||||
last = int(Trims[i - 1][1])
|
||||
lastts = get_ts(last + 1, tc)
|
||||
adjacent = True if not fn1 - (last + 1) else False
|
||||
offset += fn1 - (last + 1)
|
||||
offsetts += 0 if adjacent else fn1ts - lastts
|
||||
|
||||
if input:
|
||||
# Make list with timecodes to cut audio
|
||||
if adjacent and merge:
|
||||
del audio[-1]
|
||||
elif fn1 <= max:
|
||||
audio.append(fmt_time(fn1tsaud))
|
||||
|
||||
if fn2 <= max and fn2 != 0:
|
||||
audio.append(fmt_time(fn2tsaud))
|
||||
|
||||
# Apply the offset to the trims
|
||||
fn1 -= offset
|
||||
fn2 -= offset if fn2 else 0
|
||||
fn1ts -= offsetts
|
||||
fn2ts -= offsetts if fn2 else 0
|
||||
|
||||
# Add trims and their timestamps to list
|
||||
Trims2.append([fn1, fn2])
|
||||
Trims2ts.append((fn1ts, fn2ts))
|
||||
|
||||
# Convert fps if ofps is supplied
|
||||
if outfps and fps != outfps:
|
||||
Trims2, Trims2ts = convert_fps(Trims2, tc, ofps, Trims2ts)
|
||||
|
||||
return Trims, Trimsts, Trims2, Trims2ts, audio
|
||||
|
||||
|
||||
def write_qpfile(qpfile, trims, idr=False):
|
||||
"""Writes keyframes for use in x264 from a list of Trims."""
|
||||
|
||||
with open(qpfile, "w") as qpf:
|
||||
if trims[0][0] == 0:
|
||||
del trims[0]
|
||||
for trim in trims:
|
||||
qpf.write('{0} {1}\n'.format(trim[0], 'I' if idr else 'K'))
|
||||
|
||||
def split_audio(trims, input_file, output_file=None, delay=None, sbr=False,
|
||||
merge=True, remove=True, verbose=False, test=False):
|
||||
from subprocess import call, check_output
|
||||
from sys import getfilesystemencoding
|
||||
import json
|
||||
|
||||
sep = ',+' if merge else ','
|
||||
final_part = ''
|
||||
if len(trims) % 2 != 0:
|
||||
final_part = '{}{}-'.format(sep if len(trims) > 1 else "", trims.pop())
|
||||
cuttimes = sep.join(['{}-{}'.format(trims[i], trims[i + 1]) for i in range(0,len(trims),2)])
|
||||
cuttimes += final_part
|
||||
|
||||
ident = check_output([mkvmerge, "--identify", "-F", "json", input_file])
|
||||
tid = 0
|
||||
try:
|
||||
info = json.loads(ident)
|
||||
for track in info["tracks"]:
|
||||
if track.get("type") == "audio":
|
||||
tid = track.get("id", 0)
|
||||
sbr = track.get("properties", {}).get("aac_is_sbr", False)
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# determine delay
|
||||
delre = compile('DELAY ([-]?\d+)')
|
||||
ret = delre.search(input_file)
|
||||
delay = ('{0}:{1}'.format(tid, delay if delay else ret.group(1))
|
||||
if delay or ret else None)
|
||||
|
||||
cutCmd = [mkvmerge, '-o', output_file]
|
||||
cutCmd.extend([input_file, '--split'])
|
||||
cutCmd.extend(['parts:' + cuttimes])
|
||||
if delay:
|
||||
cutCmd.extend(['--sync', delay])
|
||||
if sbr:
|
||||
cutCmd.extend(['--aac-is-sbr', str(tid)])
|
||||
|
||||
if verbose:
|
||||
print('Cutting: {0}\n'.format(
|
||||
' '.join(['"{0}"'.format(i) for i in cutCmd])))
|
||||
else:
|
||||
cutCmd.append('-q')
|
||||
|
||||
if not test:
|
||||
cutExec = call(cutCmd)
|
||||
if cutExec == 1:
|
||||
print("Mkvmerge exited with warnings: {0:d}".format(cutExec))
|
||||
elif cutExec == 2:
|
||||
exit("Failed to execute mkvmerge: {0:d}".format(cutExec))
|
||||
|
||||
if __name__ == '__main__':
|
||||
main(argv[1:])
|
BIN
bin/vfr_old.exe
Normal file
BIN
bin/vfr_old.exe
Normal file
Binary file not shown.
BIN
bin/x64/AudioSplitter2.exe
Normal file
BIN
bin/x64/AudioSplitter2.exe
Normal file
Binary file not shown.
13
bin/x64/AudioSplitter2.exe.config
Normal file
13
bin/x64/AudioSplitter2.exe.config
Normal file
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<configuration>
|
||||
<startup useLegacyV2RuntimeActivationPolicy="true">
|
||||
<supportedRuntime version="v4.0" />
|
||||
<supportedRuntime version="v2.0" />
|
||||
</startup>
|
||||
<appSettings>
|
||||
<add key="EnableWindowsFormsHighDpiAutoResizing" value="true"/>
|
||||
</appSettings>
|
||||
<runtime>
|
||||
<AppContextSwitchOverrides value="Switch.System.IO.BlockLongPaths=false;Switch.System.IO.UseLegacyPathHandling=false"/>
|
||||
</runtime>
|
||||
</configuration>
|
BIN
bin/x64/res/AS.ico
Normal file
BIN
bin/x64/res/AS.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
2
bin/x64/res/config.txt
Normal file
2
bin/x64/res/config.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
mkvtoolnix.path=C:\\Program Files (x86)\\MKVToolNix\\
|
||||
vfr=./bin/vfr.exe
|
BIN
bin/x64/res/python32.dll
Normal file
BIN
bin/x64/res/python32.dll
Normal file
Binary file not shown.
BIN
bin/x64/res/vfr.exe
Normal file
BIN
bin/x64/res/vfr.exe
Normal file
Binary file not shown.
BIN
bin/x64/res/yuki.gif
Normal file
BIN
bin/x64/res/yuki.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 318 KiB |
Loading…
Add table
Add a link
Reference in a new issue