From 174bf2f357aa345e5ca541870debe50662dc34ec Mon Sep 17 00:00:00 2001 From: Rikux3 Date: Sun, 19 Jul 2020 00:05:11 +0200 Subject: [PATCH] Initial commit --- .gitattributes | 2 + .gitignore | 353 +++++++++++++++ LICENSE | 21 + UsmToolkit.sln | 25 ++ UsmToolkit/Helpers.cs | 24 + UsmToolkit/Program.cs | 197 +++++++++ UsmToolkit/Properties/launchSettings.json | 8 + UsmToolkit/UsmToolkit.csproj | 28 ++ UsmToolkit/VGMToolbox/CriUsmStream.cs | 226 ++++++++++ UsmToolkit/VGMToolbox/Mpeg1Stream.cs | 39 ++ UsmToolkit/VGMToolbox/MpegStream.cs | 506 ++++++++++++++++++++++ UsmToolkit/VGMToolbox/SofdecStream.cs | 57 +++ UsmToolkit/build.bat | 1 + UsmToolkit/config.json | 5 + UsmToolkit/deps.json | 4 + UsmToolkit/deps/vgmtutil.dll | Bin 0 -> 52736 bytes 16 files changed, 1496 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 UsmToolkit.sln create mode 100644 UsmToolkit/Helpers.cs create mode 100644 UsmToolkit/Program.cs create mode 100644 UsmToolkit/Properties/launchSettings.json create mode 100644 UsmToolkit/UsmToolkit.csproj create mode 100644 UsmToolkit/VGMToolbox/CriUsmStream.cs create mode 100644 UsmToolkit/VGMToolbox/Mpeg1Stream.cs create mode 100644 UsmToolkit/VGMToolbox/MpegStream.cs create mode 100644 UsmToolkit/VGMToolbox/SofdecStream.cs create mode 100644 UsmToolkit/build.bat create mode 100644 UsmToolkit/config.json create mode 100644 UsmToolkit/deps.json create mode 100644 UsmToolkit/deps/vgmtutil.dll diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e645270 --- /dev/null +++ b/.gitignore @@ -0,0 +1,353 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.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 +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# 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 + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# 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 +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# 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/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# 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 +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9976aa0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Rikux3 + +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. diff --git a/UsmToolkit.sln b/UsmToolkit.sln new file mode 100644 index 0000000..fdc05a9 --- /dev/null +++ b/UsmToolkit.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30225.117 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UsmToolkit", "UsmToolkit\UsmToolkit.csproj", "{1C7F9474-2096-4039-9F6F-9B9940717D06}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {1C7F9474-2096-4039-9F6F-9B9940717D06}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1C7F9474-2096-4039-9F6F-9B9940717D06}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1C7F9474-2096-4039-9F6F-9B9940717D06}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1C7F9474-2096-4039-9F6F-9B9940717D06}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {C8EB6C12-4B73-41DD-9F88-BDB76671892C} + EndGlobalSection +EndGlobal diff --git a/UsmToolkit/Helpers.cs b/UsmToolkit/Helpers.cs new file mode 100644 index 0000000..f40fbf4 --- /dev/null +++ b/UsmToolkit/Helpers.cs @@ -0,0 +1,24 @@ +using System.Diagnostics; + +namespace UsmToolkit +{ + public static class Helpers + { + public static void ExecuteProcess(string fileName, string arguments) + { + ProcessStartInfo startInfo = new ProcessStartInfo() + { + FileName = fileName, + Arguments = arguments, + RedirectStandardOutput = true, + UseShellExecute = false, + }; + + Process process = new Process(); + process.StartInfo = startInfo; + + process.Start(); + process.WaitForExit(); + } + } +} diff --git a/UsmToolkit/Program.cs b/UsmToolkit/Program.cs new file mode 100644 index 0000000..61104ea --- /dev/null +++ b/UsmToolkit/Program.cs @@ -0,0 +1,197 @@ +using McMaster.Extensions.CommandLineUtils; +using Newtonsoft.Json; +using System; +using System.ComponentModel.DataAnnotations; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Net; +using System.Reflection; +using System.Text; +using VGMToolbox.format; + +namespace UsmToolkit +{ + [Command("UsmToolkit")] + [VersionOptionFromMember("--version", MemberName = nameof(GetVersion))] + [Subcommand(typeof(ExtractCommand), typeof(GetDependenciesCommand))] + class Program + { + + static int Main(string[] args) + { + try + { + return CommandLineApplication.Execute(args); + } + catch (FileNotFoundException e) + { + Console.WriteLine($"The file {e.FileName} cannot be found. The program will now exit."); + return 2; + } + catch (Exception e) + { + Console.WriteLine($"FATAL ERROR: {e.Message}\n{e.StackTrace}"); + return -1; + } + } + + protected int OnExecute(CommandLineApplication app) + { + app.ShowHelp(); + return 1; + } + + private static string GetVersion() + => typeof(Program).Assembly.GetCustomAttribute().InformationalVersion; + + private class ExtractCommand + { + private class JoinConfig + { + public string VideoParameter { get; set; } + public string AudioParameter { get; set; } + public string OutputFormat { get; set; } + } + + [Required] + [FileOrDirectoryExists] + [Argument(0, Description = "File or folder containing usm files")] + public string InputPath { get; set; } + + [Option(CommandOptionType.NoValue, Description = "Join files after extraction.", ShortName = "j", LongName = "join")] + public bool JoinOutput { get; set; } + + [Option(CommandOptionType.NoValue, Description = "Remove temporary m2v and audio after joining.", ShortName = "c", LongName = "clean")] + public bool CleanTempFiles { get; set; } + + protected int OnExecute(CommandLineApplication app) + { + FileAttributes attr = File.GetAttributes(InputPath); + if (attr.HasFlag(FileAttributes.Directory)) + { + foreach (var file in Directory.GetFiles(InputPath, "*.usm")) + { + Convert(file); + } + } + else + Convert(InputPath); + + return 0; + } + + private void Convert(string fileName) + { + Console.WriteLine($"File: {fileName}"); + var usmStream = new CriUsmStream(fileName); + + usmStream.DemultiplexStreams(new MpegStream.DemuxOptionsStruct() + { + AddHeader = false, + AddPlaybackHacks = false, + ExtractAudio = true, + ExtractVideo = true, + SplitAudioStreams = false + }); + + if (JoinOutput) + { + JoinOutputFile(usmStream); + } + } + + private void JoinOutputFile(CriUsmStream usmStream) + { + var audioFormat = usmStream.FinalAudioExtension; + var pureFileName = Path.GetFileNameWithoutExtension(usmStream.FilePath); + + if (audioFormat == ".adx") + { + //ffmpeg can not handle .adx from 0.2 for whatever reason + //need vgmstream to format that to wav + if (!Directory.Exists("vgmstream")) + { + Console.WriteLine("WARNING: vgmstream folder not found!"); + } + + Helpers.ExecuteProcess("vgmstream/test.exe", $"\"{Path.ChangeExtension(usmStream.FilePath, usmStream.FinalAudioExtension)}\" -o \"{Path.ChangeExtension(usmStream.FilePath, "wav")}\""); + + usmStream.FinalAudioExtension = ".wav"; + } + + if (!File.Exists("config.json")) + { + Console.WriteLine("ERROR: config.json not found!"); + return; + } + + Helpers.ExecuteProcess("ffmpeg", CreateFFmpegParameters(usmStream, pureFileName)); + + if (CleanTempFiles) + { + File.Delete(Path.ChangeExtension(usmStream.FilePath, "wav")); + File.Delete(Path.ChangeExtension(usmStream.FilePath, "adx")); + File.Delete(Path.ChangeExtension(usmStream.FilePath, "hca")); + File.Delete(Path.ChangeExtension(usmStream.FilePath, "m2v")); + } + } + + private string CreateFFmpegParameters(CriUsmStream usmStream, string pureFileName) + { + JoinConfig conf = JsonConvert.DeserializeObject(File.ReadAllText("config.json")); + + StringBuilder sb = new StringBuilder(); + sb.Append($"-i \"{Path.ChangeExtension(usmStream.FilePath, usmStream.FileExtensionVideo)}\" "); + + if (usmStream.HasAudio) + sb.Append($"-i \"{Path.ChangeExtension(usmStream.FilePath, usmStream.FinalAudioExtension)}\" "); + + sb.Append($"{conf.VideoParameter} "); + + if (usmStream.HasAudio) + sb.Append($"{conf.AudioParameter} "); + + sb.Append($"{pureFileName}.{conf.OutputFormat}"); + + return sb.ToString(); + } + } + + private class GetDependenciesCommand + { + private class DepsConfig + { + public string Vgmstream { get; set; } + public string FFmpeg { get; set; } + } + + protected int OnExecute(CommandLineApplication app) + { + DepsConfig conf = JsonConvert.DeserializeObject(File.ReadAllText("deps.json")); + WebClient client = new WebClient(); + + //ffmpeg + client.DownloadFile(conf.FFmpeg, "ffmpeg.zip"); + + using (ZipArchive archive = ZipFile.OpenRead("ffmpeg.zip")) + { + var ent = archive.Entries.FirstOrDefault(x => x.Name == "ffmpeg.exe"); + if (ent != null) + { + ent.ExtractToFile("ffmpeg.exe", true); + } + } + + File.Delete("ffmpeg.zip"); + + //vgmstream + client.DownloadFile(conf.Vgmstream, "vgmstream.zip"); + ZipFile.ExtractToDirectory("vgmstream.zip", "vgmstream"); + File.Delete("vgmstream.zip"); + + return 0; + } + } + } +} diff --git a/UsmToolkit/Properties/launchSettings.json b/UsmToolkit/Properties/launchSettings.json new file mode 100644 index 0000000..348f2c0 --- /dev/null +++ b/UsmToolkit/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "UsmToolkit": { + "commandName": "Project", + "commandLineArgs": "extract \"F:\\CUSA05787\\tresgame\\content\\crimovie\\rebellis02\\en\\cs_dw009_mv.usm\" --join" + } + } +} \ No newline at end of file diff --git a/UsmToolkit/UsmToolkit.csproj b/UsmToolkit/UsmToolkit.csproj new file mode 100644 index 0000000..10aacbb --- /dev/null +++ b/UsmToolkit/UsmToolkit.csproj @@ -0,0 +1,28 @@ + + + + Exe + netcoreapp3.1 + + + + + + + + + + deps\vgmtutil.dll + + + + + + PreserveNewest + + + PreserveNewest + + + + diff --git a/UsmToolkit/VGMToolbox/CriUsmStream.cs b/UsmToolkit/VGMToolbox/CriUsmStream.cs new file mode 100644 index 0000000..840216f --- /dev/null +++ b/UsmToolkit/VGMToolbox/CriUsmStream.cs @@ -0,0 +1,226 @@ +using System; +using System.Collections.Generic; +using System.IO; + +using VGMToolbox.util; + +namespace VGMToolbox.format +{ + public class CriUsmStream : MpegStream + { + public const string DefaultAudioExtension = ".adx"; + public const string DefaultVideoExtension = ".m2v"; + public const string HcaAudioExtension = ".hca"; + + static readonly byte[] HCA_SIG_BYTES = new byte[] { 0x48, 0x43, 0x41, 0x00 }; + + protected static readonly byte[] ALP_BYTES = new byte[] { 0x40, 0x41, 0x4C, 0x50 }; + protected static readonly byte[] CRID_BYTES = new byte[] { 0x43, 0x52, 0x49, 0x44 }; + protected static readonly byte[] SFV_BYTES = new byte[] { 0x40, 0x53, 0x46, 0x56 }; + protected static readonly byte[] SFA_BYTES = new byte[] { 0x40, 0x53, 0x46, 0x41 }; + protected static readonly byte[] SBT_BYTES = new byte[] { 0x40, 0x53, 0x42, 0x54 }; + protected static readonly byte[] CUE_BYTES = new byte[] { 0x40, 0x43, 0x55, 0x45 }; + + protected static readonly byte[] UTF_BYTES = new byte[] { 0x40, 0x55, 0x54, 0x46 }; + + protected static readonly byte[] HEADER_END_BYTES = + new byte[] { 0x23, 0x48, 0x45, 0x41, 0x44, 0x45, 0x52, 0x20, + 0x45, 0x4E, 0x44, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x00 }; + + protected static readonly byte[] METADATA_END_BYTES = + new byte[] { 0x23, 0x4D, 0x45, 0x54, 0x41, 0x44, 0x41, 0x54, + 0x41, 0x20, 0x45, 0x4E, 0x44, 0x20, 0x20, 0x20, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x00 }; + + protected static readonly byte[] CONTENTS_END_BYTES = + new byte[] { 0x23, 0x43, 0x4F, 0x4E, 0x54, 0x45, 0x4E, 0x54, + 0x53, 0x20, 0x45, 0x4E, 0x44, 0x20, 0x20, 0x20, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x00 }; + + public CriUsmStream(string path) + : base(path) + { + this.UsesSameIdForMultipleAudioTracks = true; + this.FileExtensionAudio = DefaultAudioExtension; + this.FileExtensionVideo = DefaultVideoExtension; + + base.BlockIdDictionary.Clear(); + base.BlockIdDictionary[BitConverter.ToUInt32(ALP_BYTES, 0)] = new BlockSizeStruct(PacketSizeType.SizeBytes, 4); // @ALP + base.BlockIdDictionary[BitConverter.ToUInt32(CRID_BYTES, 0)] = new BlockSizeStruct(PacketSizeType.SizeBytes, 4); // CRID + base.BlockIdDictionary[BitConverter.ToUInt32(SFV_BYTES, 0)] = new BlockSizeStruct(PacketSizeType.SizeBytes, 4); // @SFV + base.BlockIdDictionary[BitConverter.ToUInt32(SFA_BYTES, 0)] = new BlockSizeStruct(PacketSizeType.SizeBytes, 4); // @SFA + base.BlockIdDictionary[BitConverter.ToUInt32(SBT_BYTES, 0)] = new BlockSizeStruct(PacketSizeType.SizeBytes, 4); // @SBT + base.BlockIdDictionary[BitConverter.ToUInt32(CUE_BYTES, 0)] = new BlockSizeStruct(PacketSizeType.SizeBytes, 4); // @CUE + } + + protected override byte[] GetPacketStartBytes() { return CRID_BYTES; } + + protected override int GetAudioPacketHeaderSize(Stream readStream, long currentOffset) + { + UInt16 checkBytes; + OffsetDescription od = new OffsetDescription(); + + od.OffsetByteOrder = Constants.BigEndianByteOrder; + od.OffsetSize = "2"; + od.OffsetValue = "8"; + + checkBytes = (UInt16)ParseFile.GetVaryingByteValueAtRelativeOffset(readStream, od, currentOffset); + + return checkBytes; + } + protected override int GetVideoPacketHeaderSize(Stream readStream, long currentOffset) + { + UInt16 checkBytes; + OffsetDescription od = new OffsetDescription(); + + od.OffsetByteOrder = Constants.BigEndianByteOrder; + od.OffsetSize = "2"; + od.OffsetValue = "8"; + + checkBytes = (UInt16)ParseFile.GetVaryingByteValueAtRelativeOffset(readStream, od, currentOffset); + + return checkBytes; + } + + protected override bool IsThisAnAudioBlock(byte[] blockToCheck) + { + return ParseFile.CompareSegment(blockToCheck, 0, SFA_BYTES); + } + protected override bool IsThisAVideoBlock(byte[] blockToCheck) + { + return ParseFile.CompareSegment(blockToCheck, 0, SFV_BYTES); + } + + protected override byte GetStreamId(Stream readStream, long currentOffset) + { + byte streamId; + + streamId = ParseFile.ParseSimpleOffset(readStream, currentOffset + 0xC, 1)[0]; + + return streamId; + } + + protected override int GetAudioPacketFooterSize(Stream readStream, long currentOffset) + { + UInt16 checkBytes; + OffsetDescription od = new OffsetDescription(); + + od.OffsetByteOrder = Constants.BigEndianByteOrder; + od.OffsetSize = "2"; + od.OffsetValue = "0xA"; + + checkBytes = (UInt16)ParseFile.GetVaryingByteValueAtRelativeOffset(readStream, od, currentOffset); + + return checkBytes; + } + + protected override int GetVideoPacketFooterSize(Stream readStream, long currentOffset) + { + UInt16 checkBytes; + OffsetDescription od = new OffsetDescription(); + + od.OffsetByteOrder = Constants.BigEndianByteOrder; + od.OffsetSize = "2"; + od.OffsetValue = "0xA"; + + checkBytes = (UInt16)ParseFile.GetVaryingByteValueAtRelativeOffset(readStream, od, currentOffset); + + return checkBytes; + } + + protected override void DoFinalTasks(FileStream sourceFileStream, Dictionary outputFiles, bool addHeader) + { + long headerEndOffset; + long metadataEndOffset; + long headerSize; + + long footerOffset; + long footerSize; + + string sourceFileName; + string workingFile; + string fileExtension; + string destinationFileName; + + foreach (uint streamId in outputFiles.Keys) + { + sourceFileName = outputFiles[streamId].Name; + + //-------------------------- + // get header size + //-------------------------- + headerEndOffset = ParseFile.GetNextOffset(outputFiles[streamId], 0, HEADER_END_BYTES); + metadataEndOffset = ParseFile.GetNextOffset(outputFiles[streamId], 0, METADATA_END_BYTES); + + if (metadataEndOffset > headerEndOffset) + { + headerSize = metadataEndOffset + METADATA_END_BYTES.Length; + } + else + { + headerSize = headerEndOffset + METADATA_END_BYTES.Length; + } + + //----------------- + // get footer size + //----------------- + footerOffset = ParseFile.GetNextOffset(outputFiles[streamId], 0, CONTENTS_END_BYTES) - headerSize; + footerSize = outputFiles[streamId].Length - footerOffset; + + //------------------------------------------ + // check data to adjust extension if needed + //------------------------------------------ + if (this.IsThisAnAudioBlock(BitConverter.GetBytes(streamId & 0xFFFFFFF0))) // may need to change mask if more than 0xF streams + { + byte[] checkBytes = ParseFile.ParseSimpleOffset(outputFiles[streamId], headerSize, 4); + + if (ParseFile.CompareSegment(checkBytes, 0, SofdecStream.AixSignatureBytes)) + { + fileExtension = SofdecStream.AixAudioExtension; + } + else if (checkBytes[0] == 0x80) + { + fileExtension = SofdecStream.AdxAudioExtension; + } + else if (ParseFile.CompareSegment(checkBytes, 0, HCA_SIG_BYTES)) + { + fileExtension = HcaAudioExtension; + } + else + { + fileExtension = ".bin"; + } + + this.FinalAudioExtension = fileExtension; + this.HasAudio = true; + } + else + { + fileExtension = Path.GetExtension(sourceFileName); + } + + outputFiles[streamId].Close(); + outputFiles[streamId].Dispose(); + + workingFile = FileUtil.RemoveChunkFromFile(sourceFileName, 0, headerSize); + File.Copy(workingFile, sourceFileName, true); + File.Delete(workingFile); + + workingFile = FileUtil.RemoveChunkFromFile(sourceFileName, footerOffset, footerSize); + destinationFileName = Path.ChangeExtension(sourceFileName, fileExtension); + destinationFileName = destinationFileName.Substring(0, destinationFileName.LastIndexOf("_"))+fileExtension; + File.Copy(workingFile, destinationFileName, true); + File.Delete(workingFile); + + if ((sourceFileName != destinationFileName) && (File.Exists(sourceFileName))) + { + File.Delete(sourceFileName); + } + } + } + } +} diff --git a/UsmToolkit/VGMToolbox/Mpeg1Stream.cs b/UsmToolkit/VGMToolbox/Mpeg1Stream.cs new file mode 100644 index 0000000..18e33fb --- /dev/null +++ b/UsmToolkit/VGMToolbox/Mpeg1Stream.cs @@ -0,0 +1,39 @@ +using System; +using System.IO; + +namespace VGMToolbox.format +{ + public class Mpeg1Stream : MpegStream + { + public const string DefaultAudioExtension = ".mp2"; + public const string DefaultVideoExtension = ".m1v"; + + public Mpeg1Stream(string path) + : base(path) + { + this.FileExtensionAudio = DefaultAudioExtension; + this.FileExtensionVideo = DefaultVideoExtension; + + base.BlockIdDictionary[BitConverter.ToUInt32(MpegStream.PacketStartBytes, 0)] = new BlockSizeStruct(PacketSizeType.Static, 0xC); // Pack Header + } + + protected override int GetAudioPacketHeaderSize(Stream readStream, long currentOffset) + { + int paddingByteCount = 0; + readStream.Position = currentOffset + 6; + + // skip stuffing bytes + while (readStream.ReadByte() == 0xFF) + { + paddingByteCount++; + } + + return paddingByteCount + 7; + } + + protected override int GetVideoPacketHeaderSize(Stream readStream, long currentOffset) + { + return 0xC; + } + } +} diff --git a/UsmToolkit/VGMToolbox/MpegStream.cs b/UsmToolkit/VGMToolbox/MpegStream.cs new file mode 100644 index 0000000..80c4da1 --- /dev/null +++ b/UsmToolkit/VGMToolbox/MpegStream.cs @@ -0,0 +1,506 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +using VGMToolbox.util; + +namespace VGMToolbox.format +{ + public abstract class MpegStream + { + protected static readonly byte[] PacketStartBytes = new byte[] { 0x00, 0x00, 0x01, 0xBA }; + protected static readonly byte[] PacketEndBytes = new byte[] { 0x00, 0x00, 0x01, 0xB9 }; + + public MpegStream(string path) + { + this.FilePath = path; + this.UsesSameIdForMultipleAudioTracks = false; + this.SubTitleExtractionSupported = false; + this.BlockSizeIsLittleEndian = false; + + //******************** + // Add Slice Packets + //******************** + byte[] sliceBytes; + uint sliceBytesValue; + BlockSizeStruct blockSize = new BlockSizeStruct(PacketSizeType.Static, 0xE); + + for (byte i = 0; i <= 0xAF; i++) + { + sliceBytes = new byte[] { 0x00, 0x00, 0x01, i }; + sliceBytesValue = BitConverter.ToUInt32(sliceBytes, 0); + this.BlockIdDictionary.Add(sliceBytesValue, blockSize); + } + } + + public enum PacketSizeType + { + Static, + SizeBytes, + Eof + } + + public struct MpegDemuxOptions + { + public bool AddHeader { set; get; } + } + + public struct BlockSizeStruct + { + public PacketSizeType SizeType; + public int Size; + + public BlockSizeStruct(PacketSizeType sizeTypeValue, int sizeValue) + { + this.SizeType = sizeTypeValue; + this.Size = sizeValue; + } + } + + public struct DemuxOptionsStruct + { + public bool ExtractVideo { set; get; } + public bool ExtractAudio { set; get; } + + public bool AddHeader { set; get; } + public bool SplitAudioStreams { set; get; } + public bool AddPlaybackHacks { set; get; } + } + + #region Dictionary Initialization + + protected Dictionary BlockIdDictionary = + new Dictionary + { + //******************** + // System Packets + //******************** + {BitConverter.ToUInt32(MpegStream.PacketEndBytes, 0), new BlockSizeStruct(PacketSizeType.Eof, -1)}, // Program End + {BitConverter.ToUInt32(MpegStream.PacketStartBytes, 0), new BlockSizeStruct(PacketSizeType.Static, 0xE)}, // Pack Header + {BitConverter.ToUInt32(new byte[] { 0x00, 0x00, 0x01, 0xBB }, 0), new BlockSizeStruct(PacketSizeType.SizeBytes, 2)}, // System Header, two bytes following equal length (Big Endian) + {BitConverter.ToUInt32(new byte[] { 0x00, 0x00, 0x01, 0xBD }, 0), new BlockSizeStruct(PacketSizeType.SizeBytes, 2)}, // Private Stream, two bytes following equal length (Big Endian) + {BitConverter.ToUInt32(new byte[] { 0x00, 0x00, 0x01, 0xBE }, 0), new BlockSizeStruct(PacketSizeType.SizeBytes, 2)}, // Padding Stream, two bytes following equal length (Big Endian) + {BitConverter.ToUInt32(new byte[] { 0x00, 0x00, 0x01, 0xBF }, 0), new BlockSizeStruct(PacketSizeType.SizeBytes, 2)}, // Private Stream, two bytes following equal length (Big Endian) + + //**************************** + // Audio Streams + //**************************** + {BitConverter.ToUInt32(new byte[] { 0x00, 0x00, 0x01, 0xC0 }, 0), new BlockSizeStruct(PacketSizeType.SizeBytes, 2)}, // Audio Stream, two bytes following equal length (Big Endian) + {BitConverter.ToUInt32(new byte[] { 0x00, 0x00, 0x01, 0xC1 }, 0), new BlockSizeStruct(PacketSizeType.SizeBytes, 2)}, // Audio Stream, two bytes following equal length (Big Endian) + {BitConverter.ToUInt32(new byte[] { 0x00, 0x00, 0x01, 0xC2 }, 0), new BlockSizeStruct(PacketSizeType.SizeBytes, 2)}, // Audio Stream, two bytes following equal length (Big Endian) + {BitConverter.ToUInt32(new byte[] { 0x00, 0x00, 0x01, 0xC3 }, 0), new BlockSizeStruct(PacketSizeType.SizeBytes, 2)}, // Audio Stream, two bytes following equal length (Big Endian) + {BitConverter.ToUInt32(new byte[] { 0x00, 0x00, 0x01, 0xC4 }, 0), new BlockSizeStruct(PacketSizeType.SizeBytes, 2)}, // Audio Stream, two bytes following equal length (Big Endian) + {BitConverter.ToUInt32(new byte[] { 0x00, 0x00, 0x01, 0xC5 }, 0), new BlockSizeStruct(PacketSizeType.SizeBytes, 2)}, // Audio Stream, two bytes following equal length (Big Endian) + {BitConverter.ToUInt32(new byte[] { 0x00, 0x00, 0x01, 0xC6 }, 0), new BlockSizeStruct(PacketSizeType.SizeBytes, 2)}, // Audio Stream, two bytes following equal length (Big Endian) + {BitConverter.ToUInt32(new byte[] { 0x00, 0x00, 0x01, 0xC7 }, 0), new BlockSizeStruct(PacketSizeType.SizeBytes, 2)}, // Audio Stream, two bytes following equal length (Big Endian) + {BitConverter.ToUInt32(new byte[] { 0x00, 0x00, 0x01, 0xC8 }, 0), new BlockSizeStruct(PacketSizeType.SizeBytes, 2)}, // Audio Stream, two bytes following equal length (Big Endian) + {BitConverter.ToUInt32(new byte[] { 0x00, 0x00, 0x01, 0xC9 }, 0), new BlockSizeStruct(PacketSizeType.SizeBytes, 2)}, // Audio Stream, two bytes following equal length (Big Endian) + {BitConverter.ToUInt32(new byte[] { 0x00, 0x00, 0x01, 0xCA }, 0), new BlockSizeStruct(PacketSizeType.SizeBytes, 2)}, // Audio Stream, two bytes following equal length (Big Endian) + {BitConverter.ToUInt32(new byte[] { 0x00, 0x00, 0x01, 0xCB }, 0), new BlockSizeStruct(PacketSizeType.SizeBytes, 2)}, // Audio Stream, two bytes following equal length (Big Endian) + {BitConverter.ToUInt32(new byte[] { 0x00, 0x00, 0x01, 0xCC }, 0), new BlockSizeStruct(PacketSizeType.SizeBytes, 2)}, // Audio Stream, two bytes following equal length (Big Endian) + {BitConverter.ToUInt32(new byte[] { 0x00, 0x00, 0x01, 0xCD }, 0), new BlockSizeStruct(PacketSizeType.SizeBytes, 2)}, // Audio Stream, two bytes following equal length (Big Endian) + {BitConverter.ToUInt32(new byte[] { 0x00, 0x00, 0x01, 0xCE }, 0), new BlockSizeStruct(PacketSizeType.SizeBytes, 2)}, // Audio Stream, two bytes following equal length (Big Endian) + {BitConverter.ToUInt32(new byte[] { 0x00, 0x00, 0x01, 0xCF }, 0), new BlockSizeStruct(PacketSizeType.SizeBytes, 2)}, // Audio Stream, two bytes following equal length (Big Endian) + {BitConverter.ToUInt32(new byte[] { 0x00, 0x00, 0x01, 0xD0 }, 0), new BlockSizeStruct(PacketSizeType.SizeBytes, 2)}, // Audio Stream, two bytes following equal length (Big Endian) + {BitConverter.ToUInt32(new byte[] { 0x00, 0x00, 0x01, 0xD1 }, 0), new BlockSizeStruct(PacketSizeType.SizeBytes, 2)}, // Audio Stream, two bytes following equal length (Big Endian) + {BitConverter.ToUInt32(new byte[] { 0x00, 0x00, 0x01, 0xD2 }, 0), new BlockSizeStruct(PacketSizeType.SizeBytes, 2)}, // Audio Stream, two bytes following equal length (Big Endian) + {BitConverter.ToUInt32(new byte[] { 0x00, 0x00, 0x01, 0xD3 }, 0), new BlockSizeStruct(PacketSizeType.SizeBytes, 2)}, // Audio Stream, two bytes following equal length (Big Endian) + {BitConverter.ToUInt32(new byte[] { 0x00, 0x00, 0x01, 0xD4 }, 0), new BlockSizeStruct(PacketSizeType.SizeBytes, 2)}, // Audio Stream, two bytes following equal length (Big Endian) + {BitConverter.ToUInt32(new byte[] { 0x00, 0x00, 0x01, 0xD5 }, 0), new BlockSizeStruct(PacketSizeType.SizeBytes, 2)}, // Audio Stream, two bytes following equal length (Big Endian) + {BitConverter.ToUInt32(new byte[] { 0x00, 0x00, 0x01, 0xD6 }, 0), new BlockSizeStruct(PacketSizeType.SizeBytes, 2)}, // Audio Stream, two bytes following equal length (Big Endian) + {BitConverter.ToUInt32(new byte[] { 0x00, 0x00, 0x01, 0xD7 }, 0), new BlockSizeStruct(PacketSizeType.SizeBytes, 2)}, // Audio Stream, two bytes following equal length (Big Endian) + {BitConverter.ToUInt32(new byte[] { 0x00, 0x00, 0x01, 0xD8 }, 0), new BlockSizeStruct(PacketSizeType.SizeBytes, 2)}, // Audio Stream, two bytes following equal length (Big Endian) + {BitConverter.ToUInt32(new byte[] { 0x00, 0x00, 0x01, 0xD9 }, 0), new BlockSizeStruct(PacketSizeType.SizeBytes, 2)}, // Audio Stream, two bytes following equal length (Big Endian) + {BitConverter.ToUInt32(new byte[] { 0x00, 0x00, 0x01, 0xDA }, 0), new BlockSizeStruct(PacketSizeType.SizeBytes, 2)}, // Audio Stream, two bytes following equal length (Big Endian) + {BitConverter.ToUInt32(new byte[] { 0x00, 0x00, 0x01, 0xDB }, 0), new BlockSizeStruct(PacketSizeType.SizeBytes, 2)}, // Audio Stream, two bytes following equal length (Big Endian) + {BitConverter.ToUInt32(new byte[] { 0x00, 0x00, 0x01, 0xDC }, 0), new BlockSizeStruct(PacketSizeType.SizeBytes, 2)}, // Audio Stream, two bytes following equal length (Big Endian) + {BitConverter.ToUInt32(new byte[] { 0x00, 0x00, 0x01, 0xDD }, 0), new BlockSizeStruct(PacketSizeType.SizeBytes, 2)}, // Audio Stream, two bytes following equal length (Big Endian) + {BitConverter.ToUInt32(new byte[] { 0x00, 0x00, 0x01, 0xDE }, 0), new BlockSizeStruct(PacketSizeType.SizeBytes, 2)}, // Audio Stream, two bytes following equal length (Big Endian) + {BitConverter.ToUInt32(new byte[] { 0x00, 0x00, 0x01, 0xDF }, 0), new BlockSizeStruct(PacketSizeType.SizeBytes, 2)}, // Audio Stream, two bytes following equal length (Big Endian) + + //**************************** + // Video Streams + //**************************** + {BitConverter.ToUInt32(new byte[] { 0x00, 0x00, 0x01, 0xE0 }, 0), new BlockSizeStruct(PacketSizeType.SizeBytes, 2)}, // Video Stream, two bytes following equal length (Big Endian) + {BitConverter.ToUInt32(new byte[] { 0x00, 0x00, 0x01, 0xE1 }, 0), new BlockSizeStruct(PacketSizeType.SizeBytes, 2)}, // Video Stream, two bytes following equal length (Big Endian) + {BitConverter.ToUInt32(new byte[] { 0x00, 0x00, 0x01, 0xE2 }, 0), new BlockSizeStruct(PacketSizeType.SizeBytes, 2)}, // Video Stream, two bytes following equal length (Big Endian) + {BitConverter.ToUInt32(new byte[] { 0x00, 0x00, 0x01, 0xE3 }, 0), new BlockSizeStruct(PacketSizeType.SizeBytes, 2)}, // Video Stream, two bytes following equal length (Big Endian) + {BitConverter.ToUInt32(new byte[] { 0x00, 0x00, 0x01, 0xE4 }, 0), new BlockSizeStruct(PacketSizeType.SizeBytes, 2)}, // Video Stream, two bytes following equal length (Big Endian) + {BitConverter.ToUInt32(new byte[] { 0x00, 0x00, 0x01, 0xE5 }, 0), new BlockSizeStruct(PacketSizeType.SizeBytes, 2)}, // Video Stream, two bytes following equal length (Big Endian) + {BitConverter.ToUInt32(new byte[] { 0x00, 0x00, 0x01, 0xE6 }, 0), new BlockSizeStruct(PacketSizeType.SizeBytes, 2)}, // Video Stream, two bytes following equal length (Big Endian) + {BitConverter.ToUInt32(new byte[] { 0x00, 0x00, 0x01, 0xE7 }, 0), new BlockSizeStruct(PacketSizeType.SizeBytes, 2)}, // Video Stream, two bytes following equal length (Big Endian) + {BitConverter.ToUInt32(new byte[] { 0x00, 0x00, 0x01, 0xE8 }, 0), new BlockSizeStruct(PacketSizeType.SizeBytes, 2)}, // Video Stream, two bytes following equal length (Big Endian) + {BitConverter.ToUInt32(new byte[] { 0x00, 0x00, 0x01, 0xE9 }, 0), new BlockSizeStruct(PacketSizeType.SizeBytes, 2)}, // Video Stream, two bytes following equal length (Big Endian) + {BitConverter.ToUInt32(new byte[] { 0x00, 0x00, 0x01, 0xEA }, 0), new BlockSizeStruct(PacketSizeType.SizeBytes, 2)}, // Video Stream, two bytes following equal length (Big Endian) + {BitConverter.ToUInt32(new byte[] { 0x00, 0x00, 0x01, 0xEB }, 0), new BlockSizeStruct(PacketSizeType.SizeBytes, 2)}, // Video Stream, two bytes following equal length (Big Endian) + {BitConverter.ToUInt32(new byte[] { 0x00, 0x00, 0x01, 0xEC }, 0), new BlockSizeStruct(PacketSizeType.SizeBytes, 2)}, // Video Stream, two bytes following equal length (Big Endian) + {BitConverter.ToUInt32(new byte[] { 0x00, 0x00, 0x01, 0xED }, 0), new BlockSizeStruct(PacketSizeType.SizeBytes, 2)}, // Video Stream, two bytes following equal length (Big Endian) + {BitConverter.ToUInt32(new byte[] { 0x00, 0x00, 0x01, 0xEE }, 0), new BlockSizeStruct(PacketSizeType.SizeBytes, 2)}, // Video Stream, two bytes following equal length (Big Endian) + {BitConverter.ToUInt32(new byte[] { 0x00, 0x00, 0x01, 0xEF }, 0), new BlockSizeStruct(PacketSizeType.SizeBytes, 2)}, // Video Stream, two bytes following equal length (Big Endian) + }; + #endregion + + public string FilePath { get; set; } + public string FileExtensionAudio { get; set; } + public string FileExtensionVideo { get; set; } + + public bool HasAudio { get; set; } + public string FinalAudioExtension { get; set; } + + protected Dictionary StreamIdFileType = new Dictionary(); + + public bool UsesSameIdForMultipleAudioTracks { set; get; } // for PMF/PAM/DVD, who use 000001BD for all audio tracks + public bool SubTitleExtractionSupported { set; get; } // assume not supported. + + public bool BlockSizeIsLittleEndian { set; get; } + + protected virtual byte[] GetPacketStartBytes() { return MpegStream.PacketStartBytes; } + + protected virtual byte[] GetPacketEndBytes() { return MpegStream.PacketEndBytes; } + + protected abstract int GetAudioPacketHeaderSize(Stream readStream, long currentOffset); + + protected virtual int GetAudioPacketSubHeaderSize(Stream readStream, long currentOffset, byte streamId) { return 0; } + + protected abstract int GetVideoPacketHeaderSize(Stream readStream, long currentOffset); + + protected virtual int GetAudioPacketFooterSize(Stream readStream, long currentOffset) { return 0; } + + protected virtual int GetVideoPacketFooterSize(Stream readStream, long currentOffset) { return 0; } + + protected virtual bool IsThisAnAudioBlock(byte[] blockToCheck) + { + return ((blockToCheck[3] >= 0xC0) && + (blockToCheck[3] <= 0xDF)); + } + + protected virtual bool IsThisAVideoBlock(byte[] blockToCheck) + { + return ((blockToCheck[3] >= 0xE0) && (blockToCheck[3] <= 0xEF)); + } + + protected virtual bool IsThisASubPictureBlock(byte[] blockToCheck) + { + return ((blockToCheck[3] >= 0xE0) && (blockToCheck[3] <= 0xEF)); + } + + protected virtual string GetAudioFileExtension(Stream readStream, long currentOffset) + { + return this.FileExtensionAudio; + } + + protected virtual string GetVideoFileExtension(Stream readStream, long currentOffset) + { + return this.FileExtensionVideo; + } + + protected virtual byte GetStreamId(Stream readStream, long currentOffset) { return 0; } + + protected virtual long GetStartOffset(Stream readStream, long currentOffset) { return 0; } + + protected virtual void DoFinalTasks(FileStream sourceFileStream, Dictionary outputFiles, bool addHeader) + { + + } + + public virtual void DemultiplexStreams(DemuxOptionsStruct demuxOptions) + { + using (FileStream fs = File.OpenRead(this.FilePath)) + { + long fileSize = fs.Length; + long currentOffset = 0; + + byte[] currentBlockId; + uint currentBlockIdVal; + byte[] currentBlockIdNaming; + + BlockSizeStruct blockStruct = new BlockSizeStruct(); + byte[] blockSizeArray; + uint blockSize; + + int audioBlockSkipSize; + int videoBlockSkipSize; + + int audioBlockFooterSize; + int videoBlockFooterSize; + + int cutSize; + + bool eofFlagFound = false; + + Dictionary streamOutputWriters = new Dictionary(); + string outputFileName; + + byte streamId = 0; // for types that have multiple streams in the same block ID + uint currentStreamKey; // hash key for each file + bool isAudioBlock; + string audioFileExtension; + + // look for first packet + currentOffset = this.GetStartOffset(fs, currentOffset); + currentOffset = ParseFile.GetNextOffset(fs, currentOffset, this.GetPacketStartBytes()); + + if (currentOffset != -1) + { + while (currentOffset < fileSize) + { +#if DEBUG + //if (currentOffset == 0x414080e) + //{ + // int gggg = 1; + //} + + //// hack for bad data (ni no kuni s09.pam) + //if ((currentOffset & 1) == 1) + //{ + // currentOffset = MathUtil.RoundUpToByteAlignment(currentOffset, 0x800); + //} +#endif + + try + { + // get the current block + currentBlockId = ParseFile.ParseSimpleOffset(fs, currentOffset, 4); + + // get value to use as key to hash table + currentBlockIdVal = BitConverter.ToUInt32(currentBlockId, 0); + + if (BlockIdDictionary.ContainsKey(currentBlockIdVal)) + { + // get info about this block type + blockStruct = BlockIdDictionary[currentBlockIdVal]; + + switch (blockStruct.SizeType) + { + ///////////////////// + // Static Block Size + ///////////////////// + case PacketSizeType.Static: + currentOffset += blockStruct.Size; // skip this block + break; + + ////////////////// + // End of Stream + ////////////////// + case PacketSizeType.Eof: + eofFlagFound = true; // set EOF block found so we can exit the loop + break; + + ////////////////////// + // Varying Block Size + ////////////////////// + case PacketSizeType.SizeBytes: + + // Get the block size + blockSizeArray = ParseFile.ParseSimpleOffset(fs, currentOffset + currentBlockId.Length, blockStruct.Size); + + if (!this.BlockSizeIsLittleEndian) + { + Array.Reverse(blockSizeArray); + } + + switch (blockStruct.Size) + { + case 4: + blockSize = (uint)BitConverter.ToUInt32(blockSizeArray, 0); + break; + case 2: + blockSize = (uint)BitConverter.ToUInt16(blockSizeArray, 0); + break; + case 1: + blockSize = (uint)blockSizeArray[0]; + break; + default: + throw new ArgumentOutOfRangeException(String.Format("Unhandled size block size.{0}", Environment.NewLine)); + } + + + // if block type is audio or video, extract it + isAudioBlock = this.IsThisAnAudioBlock(currentBlockId); + + if ((demuxOptions.ExtractAudio && isAudioBlock) || + (demuxOptions.ExtractVideo && this.IsThisAVideoBlock(currentBlockId))) + { + // reset stream id + streamId = 0; + + // if audio block, get the stream number from the queue + if (isAudioBlock && this.UsesSameIdForMultipleAudioTracks) + { + streamId = this.GetStreamId(fs, currentOffset); + currentStreamKey = (streamId | currentBlockIdVal); + } + else + { + currentStreamKey = currentBlockIdVal; + } + + // check if we've already started parsing this stream + if (!streamOutputWriters.ContainsKey(currentStreamKey)) + { + // convert block id to little endian for naming + currentBlockIdNaming = BitConverter.GetBytes(currentStreamKey); + Array.Reverse(currentBlockIdNaming); + + // build output file name + outputFileName = Path.GetFileNameWithoutExtension(this.FilePath); + outputFileName = outputFileName + "_" + BitConverter.ToUInt32(currentBlockIdNaming, 0).ToString("X8"); + + // add proper extension + if (this.IsThisAnAudioBlock(currentBlockId)) + { + audioFileExtension = this.GetAudioFileExtension(fs, currentOffset); + outputFileName += audioFileExtension; + + if (!this.StreamIdFileType.ContainsKey(streamId)) + { + this.StreamIdFileType.Add(streamId, audioFileExtension); + } + } + else + { + this.FileExtensionVideo = this.GetVideoFileExtension(fs, currentOffset); + outputFileName += this.FileExtensionVideo; + } + + // add output directory + outputFileName = Path.Combine(Path.GetDirectoryName(this.FilePath), outputFileName); + + // add an output stream for writing + streamOutputWriters[currentStreamKey] = new FileStream(outputFileName, FileMode.Create, FileAccess.ReadWrite); + } + + // write the block + if (this.IsThisAnAudioBlock(currentBlockId)) + { + // write audio + audioBlockSkipSize = this.GetAudioPacketHeaderSize(fs, currentOffset) + GetAudioPacketSubHeaderSize(fs, currentOffset, streamId); + audioBlockFooterSize = this.GetAudioPacketFooterSize(fs, currentOffset); + cutSize = (int)(blockSize - audioBlockSkipSize - audioBlockFooterSize); + if (cutSize > 0) + { + streamOutputWriters[currentStreamKey].Write(ParseFile.ParseSimpleOffset(fs, currentOffset + currentBlockId.Length + blockSizeArray.Length + audioBlockSkipSize, (int)(blockSize - audioBlockSkipSize)), 0, cutSize); + } +#if DEBUG + //else + //{ + // int aaa = 1; + //} +#endif + } + else + { + // write video + videoBlockSkipSize = this.GetVideoPacketHeaderSize(fs, currentOffset); + videoBlockFooterSize = this.GetVideoPacketFooterSize(fs, currentOffset); + cutSize = (int)(blockSize - videoBlockSkipSize - videoBlockFooterSize); + if (cutSize > 0) + { + streamOutputWriters[currentStreamKey].Write(ParseFile.ParseSimpleOffset(fs, currentOffset + currentBlockId.Length + blockSizeArray.Length + videoBlockSkipSize, (int)(blockSize - videoBlockSkipSize)), 0, cutSize); + } +#if DEBUG + //else + //{ + // int vvv = 1; + //} +#endif + } + } + + // move to next block + currentOffset += currentBlockId.Length + blockSizeArray.Length + blockSize; + blockSizeArray = new byte[] { }; + break; + default: + break; + } + } + else // this is an undexpected block type + { + this.closeAllWriters(streamOutputWriters); + Array.Reverse(currentBlockId); + throw new FormatException(String.Format("Block ID at 0x{0} not found in table: 0x{1}", currentOffset.ToString("X8"), BitConverter.ToUInt32(currentBlockId, 0).ToString("X8"))); + } + + // exit loop if EOF block found + if (eofFlagFound) + { + break; + } + } + catch (Exception _ex) + { + this.closeAllWriters(streamOutputWriters); + throw new Exception(String.Format("Error parsing file at offset {0), '{1}'", currentOffset.ToString("X8"), _ex.Message), _ex); + } + } // while (currentOffset < fileSize) + } + else + { + this.closeAllWriters(streamOutputWriters); + throw new FormatException(String.Format("Cannot find Pack Header for file: {0}{1}", Path.GetFileName(this.FilePath), Environment.NewLine)); + } + + /////////////////////////////////// + // Perform any final tasks needed + /////////////////////////////////// + this.DoFinalTasks(fs, streamOutputWriters, demuxOptions.AddHeader); + + ////////////////////////// + // close all open writers + ////////////////////////// + this.closeAllWriters(streamOutputWriters); + + } // using (FileStream fs = File.OpenRead(path)) + } + + private void closeAllWriters(Dictionary writers) + { + ////////////////////////// + // close all open writers + ////////////////////////// + foreach (uint b in writers.Keys) + { + if (writers[b].CanRead) + { + writers[b].Close(); + writers[b].Dispose(); + } + } + } + + public static int GetMpegStreamType(string path) + { + int mpegType = -1; + + using (FileStream fs = File.OpenRead(path)) + { + // look for first packet + long currentOffset = ParseFile.GetNextOffset(fs, 0, MpegStream.PacketStartBytes); + + if (currentOffset != -1) + { + currentOffset += 4; + fs.Position = currentOffset; + byte idByte = (byte)fs.ReadByte(); + + if ((int)ByteConversion.GetHighNibble(idByte) == 2) + { + mpegType = 1; + } + else if ((int)ByteConversion.GetHighNibble(idByte) == 4) + { + mpegType = 2; + } + } + else + { + throw new FormatException(String.Format("Cannot find Pack Header for file: {0}{1}", Path.GetFileName(path), Environment.NewLine)); + } + } + + return mpegType; + } + } +} diff --git a/UsmToolkit/VGMToolbox/SofdecStream.cs b/UsmToolkit/VGMToolbox/SofdecStream.cs new file mode 100644 index 0000000..3f92928 --- /dev/null +++ b/UsmToolkit/VGMToolbox/SofdecStream.cs @@ -0,0 +1,57 @@ +using System; +using System.IO; +using VGMToolbox.util; + +namespace VGMToolbox.format +{ + public class SofdecStream : Mpeg1Stream + { + new public const string DefaultVideoExtension = ".m2v"; + + public const string AdxAudioExtension = ".adx"; + public const string AixAudioExtension = ".aix"; + public const string Ac3AudioExtension = ".ac3"; + + public static readonly byte[] AixSignatureBytes = new byte[] { 0x41, 0x49, 0x58, 0x46 }; + public static readonly byte[] Ac3SignatureBytes = new byte[] { 0x0B, 0x77 }; + + public SofdecStream(string path): base(path) + { + this.FileExtensionAudio = AdxAudioExtension; + this.FileExtensionVideo = DefaultVideoExtension; + } + + protected override string GetAudioFileExtension(Stream readStream, long currentOffset) + { + string fileExtension; + byte[] checkBytes, checkBytesAc3; + + int headerSize = this.GetAudioPacketHeaderSize(readStream, currentOffset); + checkBytes = ParseFile.ParseSimpleOffset(readStream, (currentOffset + 6 + headerSize), 4); + + if (ParseFile.CompareSegment(checkBytes, 0, AixSignatureBytes)) + { + fileExtension = AixAudioExtension; + } + else if (checkBytes[0] == 0x80) + { + fileExtension = AdxAudioExtension; + } + else + { + checkBytesAc3 = ParseFile.ParseSimpleOffset(readStream, (currentOffset + 6 + headerSize), 2); + + if (ParseFile.CompareSegment(checkBytesAc3, 0, Ac3SignatureBytes)) + { + fileExtension = Ac3AudioExtension; + } + else + { + fileExtension = ".bin"; + } + } + + return fileExtension; + } + } +} diff --git a/UsmToolkit/build.bat b/UsmToolkit/build.bat new file mode 100644 index 0000000..cd802ee --- /dev/null +++ b/UsmToolkit/build.bat @@ -0,0 +1 @@ +dotnet publish --configuration Release --framework netcoreapp3.1 --output publish /p:DebugType=None /p:DebugSymbols=false \ No newline at end of file diff --git a/UsmToolkit/config.json b/UsmToolkit/config.json new file mode 100644 index 0000000..b2972f1 --- /dev/null +++ b/UsmToolkit/config.json @@ -0,0 +1,5 @@ +{ + "VideoParameter" : "-c:v copy", + "AudioParameter" : "-c:a ac3 -b:a 640k -af pan='stereo|FL=FL+FC+0.5*BL+BR|FR=FR+LFE+0.5*BL+BR'", + "OutputFormat" : "mp4" +} \ No newline at end of file diff --git a/UsmToolkit/deps.json b/UsmToolkit/deps.json new file mode 100644 index 0000000..3d3bafd --- /dev/null +++ b/UsmToolkit/deps.json @@ -0,0 +1,4 @@ +{ + "Vgmstream" : "https://github.com/losnoco/vgmstream/releases/latest/download/test.zip", + "FFmpeg" : "https://ffmpeg.zeranoe.com/builds/win64/static/ffmpeg-latest-win64-static.zip" +} \ No newline at end of file diff --git a/UsmToolkit/deps/vgmtutil.dll b/UsmToolkit/deps/vgmtutil.dll new file mode 100644 index 0000000000000000000000000000000000000000..019909afdf0f13dd7aa2503bd656eaab3ff10d23 GIT binary patch literal 52736 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