Compare commits

..

6 commits

Author SHA1 Message Date
Yuuki Chan
255cf9b422 Updated binaries. 2023-10-29 21:39:35 +09:00
Yuuki Chan
80a39463cc Updated Globals.ps1 2023-10-29 21:39:20 +09:00
Yuuki Chan
d70604ae90 Updated MainForm.psf 2023-10-29 21:39:07 +09:00
Yuuki Chan
fe525a3dd6 Fixed AudioSplitter2.ps1 to start up correctly. 2023-10-29 21:38:40 +09:00
Yuuki Chan
7365961df4 Updated README.md 2023-10-29 21:38:14 +09:00
Yuuki Chan
28e37f3440 Import project. 2023-10-29 21:27:43 +09:00
38 changed files with 7272 additions and 1 deletions

BIN
AudioSplitter2.ps1 Normal file

Binary file not shown.

1471
AudioSplitter2.ps1.bak Normal file

File diff suppressed because it is too large Load diff

13
AudioSplitter2.psproj Normal file
View file

@ -0,0 +1,13 @@
<Project Synchronized="False" SyncFilter="*.ps1;*.psm1;*.psd1;*.ps1xml;*.psf;*.pss;*.xml;*.help.txt">
<Version>2.1</Version>
<FileID>cfce16fa-6b4e-4338-a517-88dba9b07e2c</FileID>
<ProjectType>0</ProjectType>
<RememberPowerShellVersion>True</RememberPowerShellVersion>
<PowerShellVersion>Local Machine - PowerShell V5 (64 Bit)</PowerShellVersion>
<Folders />
<Files>
<File Build="0">Startup.pss</File>
<File Build="0" Shared="True" ReferenceFunction="Invoke-Globals_ps1">Globals.ps1</File>
<File Build="0" ReferenceFunction="Show-MainForm_psf">MainForm.psf</File>
</Files>
</Project>

11
AudioSplitter2.psproj.bak Normal file
View file

@ -0,0 +1,11 @@
<Project Synchronized="False" SyncFilter="*.ps1;*.psm1;*.psd1;*.ps1xml;*.psf;*.pss;*.xml;*.help.txt">
<Version>2.0</Version>
<FileID>cfce16fa-6b4e-4338-a517-88dba9b07e2c</FileID>
<ProjectType>0</ProjectType>
<Folders />
<Files>
<File Build="0">Startup.pss</File>
<File Build="0" Shared="True" ReferenceFunction="Invoke-Globals_ps1">Globals.ps1</File>
<File Build="0" ReferenceFunction="Show-MainForm_psf">MainForm.psf</File>
</Files>
</Project>

Binary file not shown.

9
AudioSplitter2.psprojs Normal file
View file

@ -0,0 +1,9 @@
<ProjectState>
<Version>1.0</Version>
<FileID>cfce16fa-6b4e-4338-a517-88dba9b07e2c</FileID>
<OpenFolders />
<OpenFiles>
<File>Globals.ps1</File>
<File>MainForm.psf</File>
</OpenFiles>
</ProjectState>

212
Globals.ps1 Normal file
View file

@ -0,0 +1,212 @@
<#
.NOTES
===========================================================================
Created with: SAPIEN Technologies, Inc., PowerShell Studio 2017 v5.4.144
Created on: 30/09/2017 06:23 PM
Created by: Yuuki-chan
Organization:
Filename: Globals.ps1
===========================================================================
.DESCRIPTION
A description of the file.
#>
Add-Type -AssemblyName System.IO.Compression.FileSystem
Add-Type -AssemblyName System.Windows.Forms
[XML]$doc = (New-Object System.Net.WebClient).DownloadString("http://update.yuuki-chan.xyz/Updater.xml")
[string]$version1 = "2.0.1.0"
[string]$version2 = $doc.Updater.AudioSplitterV2.Version
[string]$downloadUrl = $doc.Updater.AudioSplitterV2.Url
$props = ConvertFrom-StringData (Get-Content .\res\config.txt -Raw)
function Unzip
{
param ([string]$zipfile, [string]$outpath) [System.IO.Compression.ZipFile]::ExtractToDirectory($zipfile, $outpath)
}
function Get-ScriptDirectory
{
[OutputType([string])]
param ()
if ($null -ne $hostinvocation)
{
Split-Path $hostinvocation.MyCommand.path
}
else
{
Split-Path $script:MyInvocation.MyCommand.Path
}
}
[string]$ScriptDirectory = Get-ScriptDirectory
[Console]::WriteLine("Ignore (any) start-up errors. It's finnnneeee.")
function about
{
$pictureBox1 = New-Object System.Windows.Forms.PictureBox
$pictureBox1.ImageLocation = "res/yuki.gif"
$pictureBox1.SizeMode = [System.Windows.Forms.PictureBoxSizeMode]::StretchImage
$pictureBox1.BorderStyle = [System.Windows.Forms.BorderStyle]::FixedSingle
$pictureBox1.Size = New-Object System.Drawing.Size(570, 405)
$pictureBox1.Location = New-Object System.Drawing.Point(3, 3)
$label1 = New-Object System.Windows.Forms.Label
$label1.Text = "Original tool by:"
$label1.AutoSize = $true
$label1.Location = New-Object System.Drawing.Point(16, 413)
$label2 = New-Object System.Windows.Forms.Label
$label2.Text = "GUI design by:"
$label2.AutoSize = $true
$label2.Location = New-Object System.Drawing.Point(23, 443)
$label3 = New-Object System.Windows.Forms.Label
$label3.Text = "Special thanks to:"
$label3.AutoSize = $true
$label3.Location = New-Object System.Drawing.Point(0, 473)
$label4 = New-Object System.Windows.Forms.Label
$label4.Text = "PowerShell code:"
$label4.AutoSize = $true
$label4.Location = New-Object System.Drawing.Point(3, 502)
$label5 = New-Object System.Windows.Forms.Label
$label5.Text = "(v1.0.0.1)"
$label5.AutoSize = $true
$label5.Location = New-Object System.Drawing.Point(341, 443)
$linkLabel1 = New-Object System.Windows.Forms.LinkLabel
$linkLabel1.Text = "RiCON"
$linkLabel1.AutoSize = $true
$linkLabel1.Location = New-Object System.Drawing.Point(152, 413)
$linkLabel1.Add_Click({
[System.Diagnostics.Process]::Start("https://forum.doom9.org/member.php?u=48461")
})
$linkLabel2 = New-Object System.Windows.Forms.LinkLabel
$linkLabel2.Text = "VFR Chapter Creator 0.9"
$linkLabel2.AutoSize = $true
$linkLabel2.Location = New-Object System.Drawing.Point(276, 413)
$linklabel2.Add_Click({
[System.Diagnostics.Process]::Start("https://forum.doom9.org/showthread.php?t=154535")
})
$linkLabel3 = New-Object System.Windows.Forms.LinkLabel
$linkLabel3.Text = "Yuuki-chan"
$linkLabel3.AutoSize = $true
$linkLabel3.Location = New-Object System.Drawing.Point(152, 443)
$linkLabel3.Add_Click({
[System.Diagnostics.Process]::Start("https://yuuki-chan.xyz/")
})
$linkLabel4 = New-Object System.Windows.Forms.LinkLabel
$linkLabel4.Text = "[1001]"
$linkLabel4.AutoSize = $true
$linkLabel4.Location = New-Object System.Drawing.Point(276, 443)
$linkLabel4.Add_Click({
[System.Diagnostics.Process]::Start("https://yuuki-chan.xyz/")
})
$linkLabel5 = New-Object System.Windows.Forms.LinkLabel
$linkLabel5.Text = "Gabriel Logan"
$linkLabel5.AutoSize = $true
$linkLabel5.Location = New-Object System.Drawing.Point(152, 473)
$linkLabel5.Add_Click({
[System.Diagnostics.Process]::Start("http://www.mexat.com/vb/members/608471-Gabriel-Logan")
})
$linkLabel6 = New-Object System.Windows.Forms.LinkLabel
$linkLabel6.Text = "AmjadSONY"
$linkLabel6.AutoSize = $true
$linkLabel6.Location = New-Object System.Drawing.Point(276, 473)
$linkLabel6.Add_Click({
[System.Diagnostics.Process]::Start("http://www.mexat.com/vb/members/574111-%D8%A7%D9%85%D8%AC%D8%AF-%D8%B5%D9%84%D8%A7%D8%AD")
})
$linkLabel7 = New-Object System.Windows.Forms.LinkLabel
$linkLabel7.Text = "Intelligent"
$linkLabel7.AutoSize = $true
$linkLabel7.Location = New-Object System.Drawing.Point(399, 473)
$linkLabel7.Add_Click({
[System.Diagnostics.Process]::Start("http://www.mexat.com/vb/members/685155-Intelligent")
})
$linkLabel8 = New-Object System.Windows.Forms.LinkLabel
$linkLabel8.Text = "Yuuki-chan"
$linkLabel8.AutoSize = $true
$linkLabel8.Location = New-Object System.Drawing.Point(152, 502)
$linkLabel8.Add_Click({
[System.Diagnostics.Process]::Start("https://yuuki-chan.xyz/")
})
$button1 = New-Object System.Windows.Forms.Button
$button1.Text = "OK"
$button1.Location = New-Object System.Drawing.Point(225, 527)
$button1.Size = New-Object System.Drawing.Size(125, 35)
$button1.Add_Click({
$about.Close()
})
$about = New-Object System.Windows.Forms.Form
$about.Text = "About"
$about.MaximizeBox = $false
$about.MinimizeBox = $false
$about.Size = New-Object System.Drawing.Size(592, 623)
$about.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::FixedSingle
$about.Icon = [System.Drawing.Icon]::ExtractAssociatedIcon(".\res\AS.ico")
$about.Controls.Add($pictureBox1)
$about.Controls.Add($label1)
$about.Controls.Add($label2)
$about.Controls.Add($label3)
$about.Controls.Add($label4)
$about.Controls.Add($label5)
$about.Controls.Add($linkLabel1)
$about.Controls.Add($linkLabel2)
$about.Controls.Add($linkLabel3)
$about.Controls.Add($linkLabel4)
$about.Controls.Add($linkLabel5)
$about.Controls.Add($linkLabel6)
$about.Controls.Add($linkLabel7)
$about.Controls.Add($linkLabel8)
$about.Controls.Add($button1)
$about.Select()
$about.ShowDialog()
}
function checkForUpdate
{
if ($version1 -eq $version2)
{
$msgBox = [System.Windows.Forms.MessageBox]::Show("You already have the latest version installed.", "Information", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Information)
}
else
{
try
{
$WC = New-Object System.Net.WebClient
$WC.DownloadFile($downloadUrl, $version2 + ".zip");
# Unzip($version2 + ".zip", "")
}
catch
{
$em = $_.Exception.Message
$msgBox = [System.Windows.Forms.MessageBox]::Show($em, "Information", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error)
}
}
}
function forceUpdate
{
try
{
$WC = New-Object System.Net.WebClient
$WC.DownloadFile($downloadUrl, $version2 + ".zip");
# Unzip($version2 + ".zip", "")
}
catch
{
$em = $_.Exception.Message
$msgBox = [System.Windows.Forms.MessageBox]::Show($em, "Information", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error)
}
}

1367
MainForm.psf Normal file

File diff suppressed because it is too large Load diff

1489
MainForm.psf.bak Normal file

File diff suppressed because it is too large Load diff

1141
MainForm.psf.bak1 Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,8 @@
# AudioSplitterV2
AudioSplitter rewrite in PowersHELL.
AudioSplitter rewrite in PowersHELL.
# Info
This project is a work in progress, it is nowhere near completion and nothing works.

3
Start.bat Normal file
View file

@ -0,0 +1,3 @@
@echo off
PowerShell -ExecutionPolicy unrestricted -command "& { .\AudioSplitter2.ps1 }"
pause

28
Startup.pss Normal file
View file

@ -0,0 +1,28 @@
<File version="3.1">
<Code><![CDATA[function Main
{
Param ([String]$Commandline)
#--------------------------------------------------------------------------
#TODO: Add initialization script here (Load modules and check requirements)
#--------------------------------------------------------------------------
if((Show-MainForm_psf) -eq 'OK')
{
}
$script:ExitCode = 0
}]]></Code>
<Assemblies>
<Assembly>mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</Assembly>
<Assembly>System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</Assembly>
<Assembly>System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</Assembly>
<Assembly>System.Data, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</Assembly>
<Assembly>System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</Assembly>
<Assembly>System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</Assembly>
<Assembly>System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</Assembly>
</Assemblies>
</File>

35
Startup.pss.bak Normal file
View file

@ -0,0 +1,35 @@
<File version="2.1">
<Code><![CDATA[function Main
{
Param ([String]$Commandline)
#--------------------------------------------------------------------------
#TODO: Add initialization script here (Load modules and check requirements)
#--------------------------------------------------------------------------
if((Show-MainForm_psf) -eq 'OK')
{
}
$script:ExitCode = 0
}
]]></Code>
<Assemblies>
<Assembly>mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</Assembly>
<Assembly>System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</Assembly>
<Assembly>System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</Assembly>
<Assembly>System.Data, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</Assembly>
<Assembly>System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</Assembly>
<Assembly>System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</Assembly>
<Assembly>System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</Assembly>
</Assemblies>
</File>

3
Update.bat Normal file
View file

@ -0,0 +1,3 @@
@echo off
PowerShell -ExecutionPolicy unrestricted -command "& { . .\Globals.ps1; forceUpdate }"
pause

BIN
bin/python32.dll Normal file

Binary file not shown.

BIN
bin/vfr.exe Normal file

Binary file not shown.

18
bin/vfr/.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View file

16
bin/vfr/tcconv.py Normal file
View 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
View 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
View 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

Binary file not shown.

BIN
bin/x64/AudioSplitter2.exe Normal file

Binary file not shown.

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

2
bin/x64/res/config.txt Normal file
View file

@ -0,0 +1,2 @@
mkvtoolnix.path=H:\\- 1 Encode\\Encod.es\\mkvtoolnix_v1900\\
vfr.path=G:\\BACKUP\\Desktop\\Tes\\Portable Programming\\PowerShell\\AudioSplitter2\\res\\

BIN
bin/x64/res/python32.dll Normal file

Binary file not shown.

BIN
bin/x64/res/vfr.exe Normal file

Binary file not shown.

BIN
bin/x64/res/yuki.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 KiB

BIN
res/AS.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

2
res/config.txt Normal file
View file

@ -0,0 +1,2 @@
mkvtoolnix.path=H:\\- 1 Encode\\Encod.es\\mkvtoolnix_v1900\\
vfr.path=G:\\BACKUP\\Desktop\\Tes\\Portable Programming\\PowerShell\\AudioSplitter2\\res\\

BIN
res/python32.dll Normal file

Binary file not shown.

BIN
res/vfr.exe Normal file

Binary file not shown.

BIN
res/yuki.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 KiB