From 863215e8d96f8226f51208efe1029911a50a0587 Mon Sep 17 00:00:00 2001 From: Jan Arnold Date: Fri, 10 Mar 2023 14:37:09 +0100 Subject: [PATCH] Public release of v1.1.0 --- .gitignore | 222 +++ MergeSynced.sln | 25 + MergeSynced.sln.DotSettings | 2 + MergeSynced/Analysis.cs | 101 ++ MergeSynced/App.config | 14 + MergeSynced/App.xaml | 16 + MergeSynced/App.xaml.cs | 11 + MergeSynced/Audio.cs | 303 ++++ MergeSynced/CheckBoxMedia.cs | 11 + MergeSynced/ExternalProcesses.cs | 459 ++++++ MergeSynced/MainWindow.xaml | 315 ++++ MergeSynced/MainWindow.xaml.cs | 1114 ++++++++++++++ MergeSynced/MediaData.cs | 26 + MergeSynced/MergeSynced.csproj | 136 ++ MergeSynced/MergeSyncedLogo.ico | Bin 0 -> 329885 bytes MergeSynced/MergeSyncedLogoSimple.ico | Bin 0 -> 299408 bytes MergeSynced/ProcessExtensions.cs | 45 + MergeSynced/Properties/Annotations.cs | 1354 ++++++++++++++++++ MergeSynced/Properties/AssemblyInfo.cs | 53 + MergeSynced/Properties/Resources.Designer.cs | 63 + MergeSynced/Properties/Resources.resx | 117 ++ MergeSynced/Properties/Settings.Designer.cs | 26 + MergeSynced/Properties/Settings.settings | 7 + MergeSynced/packages.config | 10 + MergeSyncedLogo.ico | Bin 0 -> 329885 bytes README.md | 46 + TestFiles/Screenshot_01.png | Bin 0 -> 95597 bytes TestFiles/Screenshot_02.png | Bin 0 -> 98304 bytes TestFiles/SyncTestClip-1_566.mp4 | Bin 0 -> 12371102 bytes TestFiles/SyncTestClip.mp4 | Bin 0 -> 12780827 bytes 30 files changed, 4476 insertions(+) create mode 100644 .gitignore create mode 100644 MergeSynced.sln create mode 100644 MergeSynced.sln.DotSettings create mode 100644 MergeSynced/Analysis.cs create mode 100644 MergeSynced/App.config create mode 100644 MergeSynced/App.xaml create mode 100644 MergeSynced/App.xaml.cs create mode 100644 MergeSynced/Audio.cs create mode 100644 MergeSynced/CheckBoxMedia.cs create mode 100644 MergeSynced/ExternalProcesses.cs create mode 100644 MergeSynced/MainWindow.xaml create mode 100644 MergeSynced/MainWindow.xaml.cs create mode 100644 MergeSynced/MediaData.cs create mode 100644 MergeSynced/MergeSynced.csproj create mode 100644 MergeSynced/MergeSyncedLogo.ico create mode 100644 MergeSynced/MergeSyncedLogoSimple.ico create mode 100644 MergeSynced/ProcessExtensions.cs create mode 100644 MergeSynced/Properties/Annotations.cs create mode 100644 MergeSynced/Properties/AssemblyInfo.cs create mode 100644 MergeSynced/Properties/Resources.Designer.cs create mode 100644 MergeSynced/Properties/Resources.resx create mode 100644 MergeSynced/Properties/Settings.Designer.cs create mode 100644 MergeSynced/Properties/Settings.settings create mode 100644 MergeSynced/packages.config create mode 100644 MergeSyncedLogo.ico create mode 100644 README.md create mode 100644 TestFiles/Screenshot_01.png create mode 100644 TestFiles/Screenshot_02.png create mode 100644 TestFiles/SyncTestClip-1_566.mp4 create mode 100644 TestFiles/SyncTestClip.mp4 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1b901a7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,222 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +build/ +bld/ +[Bb]in/ +[Oo]bj/ + +# Visual Studio 2015 cache/options directory +.vs/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.vc.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +## TODO: Comment the next line if you want to checkin your +## web deploy settings but do note that will include unencrypted +## passwords +#*.pubxml + +*.publishproj + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config + +# Windows Azure Build Output +csx/ +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# LightSwitch generated files +GeneratedArtifacts/ +_Pvt_Extensions/ +ModelManifest.xml + +# Sublime +*.sublime-project +*.sublime-workspace + +# Inno scripts +*.iss diff --git a/MergeSynced.sln b/MergeSynced.sln new file mode 100644 index 0000000..e07fb1f --- /dev/null +++ b/MergeSynced.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.31829.152 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MergeSynced", "MergeSynced\MergeSynced.csproj", "{AF9CD880-7333-424F-ABF8-1CBD5C650126}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {AF9CD880-7333-424F-ABF8-1CBD5C650126}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AF9CD880-7333-424F-ABF8-1CBD5C650126}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AF9CD880-7333-424F-ABF8-1CBD5C650126}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AF9CD880-7333-424F-ABF8-1CBD5C650126}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {CB1B5859-70CE-48EE-AEA5-82D578EC85B1} + EndGlobalSection +EndGlobal diff --git a/MergeSynced.sln.DotSettings b/MergeSynced.sln.DotSettings new file mode 100644 index 0000000..0e38a03 --- /dev/null +++ b/MergeSynced.sln.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/MergeSynced/Analysis.cs b/MergeSynced/Analysis.cs new file mode 100644 index 0000000..e65a343 --- /dev/null +++ b/MergeSynced/Analysis.cs @@ -0,0 +1,101 @@ +using System; +using System.Diagnostics; +using System.Linq; +using System.Numerics; + +namespace MergeSynced +{ + public class Analysis + { + /// + /// https://stackoverflow.com/questions/70993291/calculate-the-crosscorrelation-of-two-vectors-more-efficiently + /// https://dsp.stackexchange.com/questions/736/how-do-i-implement-cross-correlation-to-prove-two-audio-files-are-similar + /// + /// Dataset A + /// Dataset B + /// Cross correlation + /// + public static void CrossCorrelation(float[] a, float[] b, out float[] c) + { + // Both arrays must be same size, if not, take smaller count + int size = a.Length < b.Length ? a.Length : b.Length; + + // Convert data to complex type and calculate norm sqrt(sum(X.^2)) ////////// + Complex[] aComp = new Complex[size]; + Complex[] bComp = new Complex[size]; + Complex normA = Complex.Zero; + Complex normB = Complex.Zero; + + for (int i = 0; i < size; i++) + { + aComp[i] = a[i]; + bComp[i] = b[i]; + + normA += Complex.Pow(aComp[i], 2); + normB += Complex.Pow(bComp[i], 2); + } + + normA = Complex.Sqrt(normA); + normB = Complex.Sqrt(normB); + Complex multipliedNorm = Complex.Multiply(normA, normB); + + // Fourier transformation of A and B //////////////////////////////////////// + //A + Stopwatch sw = Stopwatch.StartNew(); + MathNet.Numerics.IntegralTransforms.Fourier.Forward(aComp); + Debug.WriteLine($"{sw.ElapsedMilliseconds}ms for first FFT"); + sw.Restart(); + + //B + MathNet.Numerics.IntegralTransforms.Fourier.Forward(bComp); + Debug.WriteLine($"{sw.ElapsedMilliseconds}ms for second FFT"); + sw.Restart(); + + // Complex conjugation of B ///////////////////////////////////////////////// + for (int i = 0; i < size; i++) + { + bComp[i] = Complex.Conjugate(bComp[i]); + } + Debug.WriteLine($"{sw.ElapsedMilliseconds}ms complex conjugation"); + sw.Restart(); + + // Multiply FFTs //////////////////////////////////////////////////////////// + Complex[] multipliedFft = new Complex[size]; + for (int i = 0; i < size; i++) + { + multipliedFft[i] = Complex.Multiply(aComp[i], bComp[i]); + } + Debug.WriteLine($"{sw.ElapsedMilliseconds}ms for multiply FFTs"); + sw.Restart(); + + // Inverse FFT ////////////////////////////////////////////////////////////// + MathNet.Numerics.IntegralTransforms.Fourier.Inverse(multipliedFft); + Debug.WriteLine($"{sw.ElapsedMilliseconds}ms for inverse FFT"); + sw.Reset(); + + // Norm and convert complex type back to input type ///////////////////////// + c = new float[size]; + for (int i = 0; i < size; i++) + { + // Normalize to unity and use absolute values used to avoid + // checking for min values to get correct peak, also don't care if phase shifted?! + c[i] = Math.Abs(Convert.ToSingle(Complex.Divide(multipliedFft[i], multipliedNorm).Real)); + //c[i] = Math.Abs(Convert.ToSingle(multipliedFft[i].Real)); + } + } + + public static double CalculateDelay(float[] corrData, int sampleRate) + { + float max = corrData.Max(); + int maxIndex = Array.IndexOf(corrData, max); + double resFromLeft = (double)maxIndex / sampleRate; + double resFromRight = (double)(corrData.Length - maxIndex) / sampleRate; + Debug.WriteLine($"{resFromLeft}s _delay (from left)"); + Debug.WriteLine($"{resFromRight}s _delay (from right)"); + + if (resFromLeft < resFromRight) return -1 * resFromLeft; + return resFromRight; + } + + } +} \ No newline at end of file diff --git a/MergeSynced/App.config b/MergeSynced/App.config new file mode 100644 index 0000000..6249a67 --- /dev/null +++ b/MergeSynced/App.config @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/MergeSynced/App.xaml b/MergeSynced/App.xaml new file mode 100644 index 0000000..acb4266 --- /dev/null +++ b/MergeSynced/App.xaml @@ -0,0 +1,16 @@ + + + + + + diff --git a/MergeSynced/App.xaml.cs b/MergeSynced/App.xaml.cs new file mode 100644 index 0000000..166ea59 --- /dev/null +++ b/MergeSynced/App.xaml.cs @@ -0,0 +1,11 @@ +using System.Windows; + +namespace MergeSynced +{ + /// + /// Interaction logic for App.xaml + /// + public partial class App : Application + { + } +} diff --git a/MergeSynced/Audio.cs b/MergeSynced/Audio.cs new file mode 100644 index 0000000..308ca1b --- /dev/null +++ b/MergeSynced/Audio.cs @@ -0,0 +1,303 @@ +using System; +using System.Diagnostics; +using System.IO; + +namespace MergeSynced +{ + public class Audio + { + /// + /// Read wave file. Code from https://stackoverflow.com/questions/8754111/how-to-read-the-data-in-a-wav-file-to-an-array/11162668#11162668 + /// Added search for data chunks to skip unknown headers like LIST from e.g. RIFF files + /// http://soundfile.sapp.org/doc/WaveFormat/ for header reference + /// + /// Reads wav file as stereo + /// + /// Path to file name + /// Output of left channel + /// Output of right channel + /// Details of wav file + public WavHeader ReadWav(string filename, out float[] l, out float[] r) + { + l = r = null; + + try + { + using (FileStream fs = File.Open(filename, FileMode.Open)) + { + BinaryReader reader = new BinaryReader(fs); + WavHeader header = new WavHeader(); + + // chunk 0 + // reading int 32 as 4 bytes/chars for debugging header + char chunkId1 = reader.ReadChar(); + char chunkId2 = reader.ReadChar(); + char chunkId3 = reader.ReadChar(); + char chunkId4 = reader.ReadChar(); + header.FileSize = reader.ReadInt32(); + header.RiffType = reader.ReadInt32(); + + + // chunk 1 + // reading int 32 as 4 bytes/chars for debugging header + char fmtId1 = reader.ReadChar(); + char fmtId2 = reader.ReadChar(); + char fmtId3 = reader.ReadChar(); + char fmtId4 = reader.ReadChar(); + int fmtSize = reader.ReadInt32(); // min size 16 (default/legacy) + + // Additional info + int fmtCode = reader.ReadInt16(); + header.Channels = reader.ReadInt16(); + header.SampleRate = reader.ReadInt32(); + int byteRate = reader.ReadInt32(); + int fmtBlockAlign = reader.ReadInt16(); + header.BitDepth = reader.ReadInt16(); + + if (fmtSize > 16) + { + // Read any extra values that are not important to us + int fmtExtraSize = reader.ReadInt16(); + if (fmtExtraSize > 0) reader.ReadBytes(fmtExtraSize); + } + + // chunk 2 can have more header data before data + byte[] byteArray = null; + int bytes = 0; + while (reader.BaseStream.Position != reader.BaseStream.Length) + { + string dataId = new string(reader.ReadChars(4)); + bytes = reader.ReadInt32(); + + if (dataId.ToLower() != "data") + { + reader.ReadBytes(bytes); + } + else + { + byteArray = reader.ReadBytes(bytes); + break; + } + } + + if (byteArray == null) return null; + + int bytesForSamples = header.BitDepth / 8; + int nValues = bytes / bytesForSamples + 1; + + + float[] asFloat; + switch (header.BitDepth) + { + case 64: + double[] asDouble = new double[nValues]; + Buffer.BlockCopy(byteArray, 0, asDouble, 0, bytes); + asFloat = Array.ConvertAll(asDouble, e => (float)e); + break; + case 32: + asFloat = new float[nValues]; + Buffer.BlockCopy(byteArray, 0, asFloat, 0, bytes); + break; + case 16: + short[] + asInt16 = new short[nValues]; + Buffer.BlockCopy(byteArray, 0, asInt16, 0, bytes); + asFloat = Array.ConvertAll(asInt16, e => e / (float)(short.MaxValue + 1)); + break; + default: + return null; + } + + int nSamples; + switch (header.Channels) + { + case 0: + return null; + case 1: + l = asFloat; + r = null; + return header; + case 2: + // de-interleave + nSamples = nValues / 2; + l = new float[nSamples]; + r = new float[nSamples]; + for (int s = 0, v = 0; s < nSamples; s++) + { + l[s] = asFloat[v++]; + r[s] = asFloat[v++]; + } + return header; + default: + // de-interleave + nSamples = nValues / header.Channels; + l = new float[nSamples]; + r = new float[nSamples]; + for (int s = 0, v = 0; s < nSamples; s++) + { + l[s] = asFloat[v++]; + r[s] = asFloat[v++]; + v = v + header.Channels - 2; + } + return header; + } + } + } + catch (Exception ex) + { + Debug.WriteLine("...Failed to load: " + filename); + Debug.WriteLine(ex); + return null; + } + } + + /// + /// Read wave file. Code from https://stackoverflow.com/questions/8754111/how-to-read-the-data-in-a-wav-file-to-an-array/11162668#11162668 + /// Added search for data chunks to skip unknown headers like LIST from e.g. RIFF files + /// http://soundfile.sapp.org/doc/WaveFormat/ for header reference + /// + /// Reads wav file as mono + /// + /// Path to file name + /// Output of left channel + /// Details of wav file + public WavHeader ReadWav(string filename, out float[] m) + { + m = null; + + try + { + using (FileStream fs = File.Open(filename, FileMode.Open)) + { + BinaryReader reader = new BinaryReader(fs); + WavHeader header = new WavHeader(); + + // chunk 0 + // reading int 32 as 4 bytes/chars for debugging header + char chunkId1 = reader.ReadChar(); + char chunkId2 = reader.ReadChar(); + char chunkId3 = reader.ReadChar(); + char chunkId4 = reader.ReadChar(); + header.FileSize = reader.ReadInt32(); + header.RiffType = reader.ReadInt32(); + + + // chunk 1 + // reading int 32 as 4 bytes/chars for debugging header + char fmtId1 = reader.ReadChar(); + char fmtId2 = reader.ReadChar(); + char fmtId3 = reader.ReadChar(); + char fmtId4 = reader.ReadChar(); + int fmtSize = reader.ReadInt32(); // min size 16 (default/legacy) + + // Additional info + int fmtCode = reader.ReadInt16(); + header.Channels = reader.ReadInt16(); + header.SampleRate = reader.ReadInt32(); + int byteRate = reader.ReadInt32(); + int fmtBlockAlign = reader.ReadInt16(); + header.BitDepth = reader.ReadInt16(); + + if (fmtSize > 16) + { + // Read any extra values that are not important to us + int fmtExtraSize = reader.ReadInt16(); + if (fmtExtraSize > 0) reader.ReadBytes(fmtExtraSize); + } + + // chunk 2 can have more header data before data + byte[] byteArray = null; + int bytes = 0; + while (reader.BaseStream.Position != reader.BaseStream.Length) + { + string dataId = new string(reader.ReadChars(4)); + bytes = reader.ReadInt32(); + + if (dataId.ToLower() != "data") + { + reader.ReadBytes(bytes); + } + else + { + byteArray = reader.ReadBytes(bytes); + break; + } + } + + if (byteArray == null) return null; + + int bytesForSampling = header.BitDepth / 8; + int nValues = bytes / bytesForSampling + 1; + + + float[] asFloat; + switch (header.BitDepth) + { + case 64: + double[] asDouble = new double[nValues]; + Buffer.BlockCopy(byteArray, 0, asDouble, 0, bytes); + asFloat = Array.ConvertAll(asDouble, e => (float)e); + break; + case 32: + asFloat = new float[nValues]; + Buffer.BlockCopy(byteArray, 0, asFloat, 0, bytes); + break; + case 16: + short[] + asInt16 = new short[nValues]; + Buffer.BlockCopy(byteArray, 0, asInt16, 0, bytes); + asFloat = Array.ConvertAll(asInt16, e => e / (float)(short.MaxValue + 1)); + break; + default: + return null; + } + + int nSamples; + switch (header.Channels) + { + case 0: + return null; + case 1: + m = asFloat; + return header; + case 2: + // de-interleave + nSamples = nValues / 2; + m = new float[nSamples]; + for (int s = 0, v = 0; s < nSamples; s++) + { + m[s] = asFloat[v++]; + v++; + } + return header; + default: + // de-interleave + nSamples = nValues / header.Channels; + m = new float[nSamples]; + for (int s = 0, v = 0; s < nSamples; s++) + { + m[s] = asFloat[v++]; + v = v + header.Channels - 1; + } + return header; + } + } + } + catch (Exception ex) + { + Debug.WriteLine("...Failed to load: " + filename); + Debug.WriteLine(ex); + return null; + } + } + } + + public class WavHeader + { + public int FileSize; + public int RiffType; + public int Channels; + public int BitDepth; + public int SampleRate; + } +} \ No newline at end of file diff --git a/MergeSynced/CheckBoxMedia.cs b/MergeSynced/CheckBoxMedia.cs new file mode 100644 index 0000000..5835351 --- /dev/null +++ b/MergeSynced/CheckBoxMedia.cs @@ -0,0 +1,11 @@ +using System.Windows.Controls; + +namespace MergeSynced +{ + public class CheckBoxMedia : CheckBox + { + public int Index = -1; + public string LanguageId = string.Empty; + public string CodecType = string.Empty; + } +} \ No newline at end of file diff --git a/MergeSynced/ExternalProcesses.cs b/MergeSynced/ExternalProcesses.cs new file mode 100644 index 0000000..a105115 --- /dev/null +++ b/MergeSynced/ExternalProcesses.cs @@ -0,0 +1,459 @@ +using System; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Text; +using System.Windows.Controls; +using System.Windows.Media; +using Newtonsoft.Json.Linq; + +namespace MergeSynced +{ + /// + /// Calling external tools ffmpeg and mkvtoolnix to probe and merge input files. + /// + public class ExternalProcesses + { + #region Fields + + public Process FfmpegProcess; + public Process FfprobeProcess; + public bool FfmpegWasAborted; + + public Process MkvmergeProcess; + public bool MkvmergeWasAborted; + + #endregion + + #region Probing output handler + + private readonly StringBuilder _probeJson = new StringBuilder(); + public void ProbeOutputHandler(object sender, DataReceivedEventArgs e) + { + _probeJson.AppendLine(e.Data); + } + + #endregion + + #region ffmpeg + + public void CallFfmpeg(string args, DataReceivedEventHandler outputHandler, string workingDir=@"C:\temp") + { + if (FfmpegProcess != null) + { + AbortMerge(); + } + + FfmpegWasAborted = false; + FfmpegProcess = new Process(); + FfmpegProcess.StartInfo.FileName = "ffmpeg"; + FfmpegProcess.StartInfo.Arguments = args; + FfmpegProcess.StartInfo.WorkingDirectory = workingDir; + + // Options + FfmpegProcess.StartInfo.CreateNoWindow = true; + FfmpegProcess.StartInfo.UseShellExecute = false; + FfmpegProcess.StartInfo.RedirectStandardInput = true; + FfmpegProcess.StartInfo.RedirectStandardOutput = true; + FfmpegProcess.StartInfo.RedirectStandardError = true; + //_ffmpegProcess.EnableRaisingEvents = true; + //_ffmpegProcess.Exited += delegate {/* clean up*/}; + + // Receive StdOut and StdErr + FfmpegProcess.OutputDataReceived += outputHandler; + FfmpegProcess.ErrorDataReceived += outputHandler; + + // Start process + FfmpegProcess.Start(); + FfmpegProcess.BeginOutputReadLine(); + FfmpegProcess.BeginErrorReadLine(); + } + + public void CallFfprobe(string filePath, string workingDir = @"C:\temp") + { + if (FfprobeProcess != null) + { + try + { + if (!FfprobeProcess.HasExited) + { + Debug.WriteLine("Killing ffprobe..."); + FfprobeProcess.Kill(); + Debug.WriteLine("... ffprobe killed at start"); + } + } + catch (Exception e) + { + Debug.WriteLine(e); + } + finally + { + FfprobeProcess.Close(); + FfprobeProcess.Dispose(); + FfprobeProcess = null; + } + } + + FfprobeProcess = new Process(); + FfprobeProcess.StartInfo.FileName = "ffprobe"; + FfprobeProcess.StartInfo.Arguments = $"-v quiet -print_format json -show_format -show_streams -print_format json \"{filePath}\""; + FfprobeProcess.StartInfo.WorkingDirectory = workingDir; + + // Options + FfprobeProcess.StartInfo.CreateNoWindow = true; + FfprobeProcess.StartInfo.UseShellExecute = false; + FfprobeProcess.StartInfo.RedirectStandardInput = true; + FfprobeProcess.StartInfo.RedirectStandardOutput = true; + FfprobeProcess.StartInfo.RedirectStandardError = true; + + // Receive StdOut and StdErr + _probeJson.Clear(); + FfprobeProcess.OutputDataReceived += ProbeOutputHandler; + FfprobeProcess.ErrorDataReceived += ProbeOutputHandler; + + // Start process + FfprobeProcess.Start(); + FfprobeProcess.BeginOutputReadLine(); + FfprobeProcess.BeginErrorReadLine(); + } + + public bool ParseFfprobeJson(MediaData md) + { + if (md == null) return false; + md.Clear(); + + try + { + JObject json = JObject.Parse(_probeJson.ToString()); + + if (json["format"] == null || json["streams"] == null || json["format"]["duration"] == null) return false; + + // Get length of file + md.Duration = TimeSpan.FromSeconds(Convert.ToDouble(json["format"]["duration"].ToString(), new CultureInfo("en-us"))); + + bool audioTrackSelected = false; + + foreach (JToken stream in json["streams"]) + { + string language = string.Empty; + if (stream["tags"] != null) + { + if (stream["tags"]["language"] != null) language = stream["tags"]["language"].ToString(); + } + else + { + language = "unknown"; + } + + string streamInfo = + $"idx: {stream["index"]}; type: {stream["codec_type"]}; codec: {stream["codec_name"]}; language: {language};"; + Debug.WriteLine(streamInfo); + CheckBoxMedia cb = new CheckBoxMedia(); + if (int.TryParse(stream["index"]?.ToString(), out int index)) + { + cb.Index = index; + } + + cb.LanguageId = language; + cb.CodecType = stream["codec_type"]?.ToString(); + cb.Content = streamInfo; + cb.IsChecked = md.IsMainMedia; + + // Color code + switch (cb.CodecType) + { + case "audio": + cb.Background = new SolidColorBrush(Colors.LimeGreen); + ComboBoxItem co = new ComboBoxItem + { + Content = cb.Index.ToString(), + IsSelected = !audioTrackSelected + }; + md.ComboBoxItems.Add(co); + audioTrackSelected = true; + break; + case "subtitle": + cb.Background = new SolidColorBrush(Colors.Yellow); + break; + case "video": + cb.Background = new SolidColorBrush(Colors.DodgerBlue); + break; + } + + md.ListBoxItems.Add(cb); + } + } + catch (Exception e) + { + Debug.WriteLine(e); + return false; + } + + return true; + } + + #endregion + + #region mkvtoolnix + + public bool AbortMerge() + { + if (FfmpegProcess == null && MkvmergeProcess == null) return false; + + if (FfmpegProcess != null) + { + try + { + Debug.WriteLine("Sending quit signal to ffmpeg process..."); + // Get StdInput from ffmpeg process and send q to quit gracefully + StreamWriter streamWriter = FfmpegProcess.StandardInput; + streamWriter.WriteLine("q"); + + // Give process time to quit + FfmpegProcess.WaitForExit(5000); + Debug.WriteLine("Checking if ffmpeg quit gracefully"); + + if (!FfmpegProcess.HasExited) + { + Debug.WriteLine("Killing ffmpeg..."); + FfmpegProcess.Kill(); + Debug.WriteLine("... ffmpeg killed"); + } + } + catch (Exception e) + { + Debug.WriteLine(e); + } + finally + { + FfmpegProcess.Close(); + FfmpegProcess.Dispose(); + FfmpegProcess = null; + } + + FfmpegWasAborted = true; + } + + if (MkvmergeProcess == null) return true; + + try + { + Debug.WriteLine("Sending quit signal to mkvmerge process..."); + // Get StdInput from mkvmerge process and send ctrl+c + StreamWriter streamWriter = MkvmergeProcess.StandardInput; + streamWriter.WriteLine("\x3"); + + // Give process time to quit + MkvmergeProcess.WaitForExit(5000); + Debug.WriteLine("Checking if mkvmerge quit gracefully"); + + if (!MkvmergeProcess.HasExited) + { + Debug.WriteLine("Killing mkvmerge..."); + MkvmergeProcess.Kill(); + Debug.WriteLine("... mkvmerge killed"); + } + } + catch (Exception e) + { + Debug.WriteLine(e); + } + finally + { + MkvmergeProcess.Close(); + MkvmergeProcess.Dispose(); + MkvmergeProcess = null; + } + + MkvmergeWasAborted = true; + + return true; + } + + public void CallMkvmerge(string args, DataReceivedEventHandler outputHandler, string workingDir = @"C:\temp") + { + if (MkvmergeProcess != null) + { + try + { + Debug.WriteLine("Sending quit signal to mkvmerge process..."); + // Get StdInput from mkvmerge process and send ctrl+c + StreamWriter streamWriter = MkvmergeProcess.StandardInput; + streamWriter.WriteLine("\x3"); + + // Give process time to quit + MkvmergeProcess.WaitForExit(5000); + Debug.WriteLine("Checking if mkvmerge quit gracefully"); + + if (!MkvmergeProcess.HasExited) + { + Debug.WriteLine("Killing mkvmerge..."); + MkvmergeProcess.Kill(); + Debug.WriteLine("... mkvmerge killed"); + } + } + catch (Exception e) + { + Debug.WriteLine(e); + } + finally + { + MkvmergeProcess.Close(); + MkvmergeProcess.Dispose(); + MkvmergeProcess = null; + } + } + + MkvmergeWasAborted = false; + MkvmergeProcess = new Process(); + MkvmergeProcess.StartInfo.FileName = "mkvmerge"; + MkvmergeProcess.StartInfo.Arguments = args; + MkvmergeProcess.StartInfo.WorkingDirectory = workingDir; + + // Options + MkvmergeProcess.StartInfo.CreateNoWindow = true; + MkvmergeProcess.StartInfo.UseShellExecute = false; + MkvmergeProcess.StartInfo.RedirectStandardInput = true; + MkvmergeProcess.StartInfo.RedirectStandardOutput = true; + MkvmergeProcess.StartInfo.RedirectStandardError = true; + //MkvmergeProcess.EnableRaisingEvents = true; + //MkvmergeProcess.Exited += delegate {/* clean up*/}; + + // Receive StdOut and StdErr + _probeJson.Clear(); + MkvmergeProcess.OutputDataReceived += outputHandler; + MkvmergeProcess.ErrorDataReceived += outputHandler; + + // Start process + MkvmergeProcess.Start(); + MkvmergeProcess.BeginOutputReadLine(); + MkvmergeProcess.BeginErrorReadLine(); + } + + public bool ParseMkvmergeJson(MediaData md) + { + if (md == null) return false; + md.Clear(); + + try + { + JObject json = JObject.Parse(_probeJson.ToString()); + + if (json["tracks"] == null) return false; + + // Get length of file + string duration = string.Empty; + if (json["container"]?["properties"]?["duration"] != null) + duration = json["container"]["properties"]["duration"].ToString(); + // ReSharper disable once PossibleLossOfFraction + if (duration != string.Empty) md.Duration = TimeSpan.FromSeconds(Convert.ToInt32(duration.Substring(0, duration.Length - 6)) / 1000); // ns -> ms -> s + + bool audioTrackSelected = false; + + foreach (JToken stream in json["tracks"]) + { + CheckBoxMedia cb = new CheckBoxMedia + { + IsChecked = md.IsMainMedia + }; + string codecName = stream["codec"]?.ToString(); + if (stream["properties"] != null) + { + if (stream["properties"]["language"] != null) cb.LanguageId = stream["properties"]["language"].ToString(); + } + else + { + cb.LanguageId = "unknown"; + } + if (int.TryParse(stream["id"]?.ToString(), out int index)) + { + cb.Index = index; + } + cb.CodecType = stream["type"]?.ToString(); + + string streamInfo = $"idx: {cb.Index}; type: {cb.CodecType}; codec: {codecName}; language: {cb.LanguageId};"; + cb.Content = streamInfo; + Debug.WriteLine(streamInfo); + + // Color code + switch (cb.CodecType) + { + case "audio": + cb.Background = new SolidColorBrush(Colors.LimeGreen); + ComboBoxItem co = new ComboBoxItem + { + Content = cb.Index.ToString(), + IsSelected = !audioTrackSelected + }; + md.ComboBoxItems.Add(co); + audioTrackSelected = true; + break; + case "subtitles": + cb.Background = new SolidColorBrush(Colors.Yellow); + break; + case "video": + cb.Background = new SolidColorBrush(Colors.DodgerBlue); + break; + } + + md.ListBoxItems.Add(cb); + } + try + { + if (json["chapters"] != null) + { + foreach (JToken stream in json["chapters"]) + { + if (stream["num_entries"] == null || stream["num_entries"].ToObject() <= 0) continue; + CheckBoxMedia cb = new CheckBoxMedia + { + IsChecked = md.IsMainMedia, + CodecType = "chapters", + Index = 0, + Content = $"idx: n/a; type: chapters; entries: {stream["num_entries"]}", + Background = new SolidColorBrush(Colors.Silver) + }; + md.ListBoxItems.Add(cb); + } + + } + + if (json["attachments"] != null) + { + foreach (JToken stream in json["attachments"]) + { + if (stream["file_name"] == null || stream["id"] == null) continue; + CheckBoxMedia cb = new CheckBoxMedia + { + IsChecked = md.IsMainMedia, + CodecType = "attachments", + Content = $"idx: {stream["id"]}; type: attachments; file name: {stream["file_name"]}", + Background = new SolidColorBrush(Colors.DeepPink) + }; + if (int.TryParse(stream["id"]?.ToString(), out int index)) + { + cb.Index = index; + } + md.ListBoxItems.Add(cb); + } + + } + } + catch (Exception e) + { + Debug.WriteLine(e); + } + } + catch (Exception e) + { + Debug.WriteLine(e); + return false; + } + + return true; + } + + #endregion + + } +} \ No newline at end of file diff --git a/MergeSynced/MainWindow.xaml b/MergeSynced/MainWindow.xaml new file mode 100644 index 0000000..cb870d6 --- /dev/null +++ b/MergeSynced/MainWindow.xaml @@ -0,0 +1,315 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +