Add multi threading and progress percentages

This commit is contained in:
Skyth 2017-04-23 19:22:25 +03:00
parent 608f94c646
commit 2131ac6799
40 changed files with 936 additions and 296 deletions

View File

@ -1,8 +1,7 @@
# Sonic Audio Tools
Sonic Audio Tools is a set of tools for editing the audio formats seen in Sonic the Hedgehog games. Currently, it's mostly focused on CRIWARE's audio formats, this means that these tools can be used to edit files from any game, as long as they are CSB, CPK, ACB or AWB.
## Releases
[Here.](https://ci.appveyor.com/project/blueskythlikesclouds/sonicaudiotools)
You can get the binaries from Artifacts tab.
[Here.](https://ci.appveyor.com/project/blueskythlikesclouds/sonicaudiotools/build/artifacts)
## Building
1. Clone from [GitHub](https://github.com/blueskythlikesclouds/SonicAudioTools.git) `git clone https://github.com/blueskythlikesclouds/SonicAudioTools.git`

View File

@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26228.4
VisualStudioVersion = 15.0.26403.7
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SonicAudioLib", "Source\SonicAudioLib\SonicAudioLib.csproj", "{63138773-1F47-474C-9345-15EB6183ECC6}"
EndProject

View File

@ -51,9 +51,18 @@
<DesignTime>True</DesignTime>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<Compile Include="Properties\Settings.Designer.cs">
<AutoGen>True</AutoGen>
<DesignTimeSharedInput>True</DesignTimeSharedInput>
<DependentUpon>Settings.settings</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
<None Include="Properties\Settings.settings">
<Generator>SettingsSingleFileGenerator</Generator>
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
</None>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SonicAudioLib\SonicAudioLib.csproj">

View File

@ -1,6 +1,24 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2" />
</startup>
<configSections>
<sectionGroup name="userSettings" type="System.Configuration.UserSettingsGroup, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" >
<section name="AcbEditor.Properties.Settings" type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" allowExeDefinition="MachineToLocalUser" requirePermission="false" />
</sectionGroup>
</configSections>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2" />
</startup>
<userSettings>
<AcbEditor.Properties.Settings>
<setting name="EnableThreading" serializeAs="String">
<value>True</value>
</setting>
<setting name="MaxThreads" serializeAs="String">
<value>4</value>
</setting>
<setting name="BufferSize" serializeAs="String">
<value>4096</value>
</setting>
</AcbEditor.Properties.Settings>
</userSettings>
</configuration>

View File

@ -2,7 +2,11 @@
using System.IO;
using System.Windows.Forms;
using System.Text;
using System.Threading.Tasks;
using AcbEditor.Properties;
using SonicAudioLib;
using SonicAudioLib.CriMw;
using SonicAudioLib.IO;
using SonicAudioLib.Archive;
@ -22,9 +26,16 @@ namespace AcbEditor
#if !DEBUG
try
{
#endif
#endif
if (args[0].EndsWith(".acb"))
{
var extractor = new DataExtractor();
extractor.ProgressChanged += OnProgressChanged;
extractor.BufferSize = Settings.Default.BufferSize;
extractor.EnableThreading = Settings.Default.EnableThreading;
extractor.MaxThreads = Settings.Default.MaxThreads;
string baseDirectory = Path.GetDirectoryName(args[0]);
string outputDirectoryPath = Path.ChangeExtension(args[0], null);
string extAfs2ArchivePath = string.Empty;
@ -58,6 +69,7 @@ namespace AcbEditor
bool cpkMode = true;
long awbPosition = acbReader.GetPosition("AwbFile");
if (acbReader.GetLength("AwbFile") > 0)
{
using (Substream afs2Stream = acbReader.GetSubstream("AwbFile"))
@ -109,8 +121,6 @@ namespace AcbEditor
outputName += GetExtension(encodeType);
outputName = Path.Combine(outputDirectoryPath, outputName);
Console.WriteLine("Extracting {0} file with id {1}...", GetExtension(encodeType).ToUpper(), id);
if (streaming)
{
if (!found)
@ -121,7 +131,7 @@ namespace AcbEditor
else if (extCpkArchive == null && cpkMode)
{
extCpkArchive = new CriCpkArchive();
extCpkArchive.Load(extAfs2ArchivePath);
extCpkArchive.Load(extAfs2ArchivePath, Settings.Default.BufferSize);
}
EntryBase afs2Entry = null;
@ -136,12 +146,7 @@ namespace AcbEditor
afs2Entry = extAfs2Archive.GetById(id);
}
using (Stream extAfs2Stream = File.OpenRead(extAfs2ArchivePath))
using (Stream afs2EntryStream = afs2Entry.Open(extAfs2Stream))
using (Stream afs2EntryDestination = File.Create(outputName))
{
afs2EntryStream.CopyTo(afs2EntryDestination);
}
extractor.Add(extAfs2ArchivePath, outputName, afs2Entry.Position, afs2Entry.Length);
}
else
@ -158,16 +163,13 @@ namespace AcbEditor
afs2Entry = afs2Archive.GetById(id);
}
using (Substream afs2Stream = acbReader.GetSubstream("AwbFile"))
using (Stream afs2EntryStream = afs2Entry.Open(afs2Stream))
using (Stream afs2EntryDestination = File.Create(outputName))
{
afs2EntryStream.CopyTo(afs2EntryDestination);
}
extractor.Add(args[0], outputName, awbPosition + afs2Entry.Position, afs2Entry.Length);
}
}
}
}
extractor.Run();
}
else if (File.GetAttributes(args[0]).HasFlag(FileAttributes.Directory))
@ -191,11 +193,11 @@ namespace AcbEditor
if (!File.Exists(acbPath))
{
throw new Exception("Cannot find the .ACB file for this directory. Please ensure that the .ACB file is stored in the directory where this directory is.");
throw new FileNotFoundException("Cannot find the .ACB file for this directory. Please ensure that the .ACB file is stored in the directory where this directory is.");
}
CriTable acbFile = new CriTable();
acbFile.Load(acbPath);
acbFile.Load(acbPath, Settings.Default.BufferSize);
CriAfs2Archive afs2Archive = new CriAfs2Archive();
CriAfs2Archive extAfs2Archive = new CriAfs2Archive();
@ -204,6 +206,11 @@ namespace AcbEditor
CriCpkArchive extCpkArchive = new CriCpkArchive();
cpkArchive.Mode = extCpkArchive.Mode = CriCpkMode.Id;
afs2Archive.ProgressChanged += OnProgressChanged;
extAfs2Archive.ProgressChanged += OnProgressChanged;
cpkArchive.ProgressChanged += OnProgressChanged;
extCpkArchive.ProgressChanged += OnProgressChanged;
bool cpkMode = true;
byte[] awbFile = (byte[])acbFile.Rows[0]["AwbFile"];
@ -222,7 +229,7 @@ namespace AcbEditor
string inputName = id.ToString("D5");
if (streaming)
{
inputName += "_streaming";
inputName += "_streaming";
}
inputName += GetExtension(encodeType);
@ -230,11 +237,9 @@ namespace AcbEditor
if (!File.Exists(inputName))
{
throw new Exception($"Cannot find audio file with id {id} for replacement.\nPath attempt: {inputName}");
throw new FileNotFoundException($"Cannot find audio file with id {id} for replacement.\nPath attempt: {inputName}");
}
Console.WriteLine("Adding {0}...", Path.GetFileName(inputName));
if (cpkMode)
{
CriCpkEntry entry = new CriCpkEntry();
@ -278,6 +283,7 @@ namespace AcbEditor
{
Console.WriteLine("Saving internal AWB...");
acbFile.Rows[0]["AwbFile"] = cpkMode ? cpkArchive.Save() : afs2Archive.Save();
Console.WriteLine();
}
if (extAfs2Archive.Count > 0 || extCpkArchive.Count > 0)
@ -285,30 +291,19 @@ namespace AcbEditor
Console.WriteLine("Saving external AWB...");
if (cpkMode)
{
extCpkArchive.Save(awbPath);
extCpkArchive.Save(awbPath, Settings.Default.BufferSize);
}
else
{
extAfs2Archive.Save(awbPath);
byte[] afs2Header = new byte[16 +
(extAfs2Archive.Count * extAfs2Archive.IdFieldLength) +
(extAfs2Archive.Count * extAfs2Archive.PositionFieldLength) +
extAfs2Archive.PositionFieldLength];
using (FileStream fileStream = File.OpenRead(awbPath))
{
fileStream.Read(afs2Header, 0, afs2Header.Length);
}
acbFile.Rows[0]["StreamAwbAfs2Header"] = afs2Header;
extAfs2Archive.Save(awbPath, Settings.Default.BufferSize);
acbFile.Rows[0]["StreamAwbAfs2Header"] = extAfs2Archive.Header;
}
}
acbFile.WriterSettings = CriTableWriterSettings.Adx2Settings;
acbFile.Save(acbPath);
}
acbFile.Save(acbPath, Settings.Default.BufferSize);
}
#if !DEBUG
}
@ -363,5 +358,18 @@ namespace AcbEditor
return result;
}
private static string buffer = new string(' ', 17);
private static void OnProgressChanged(object sender, ProgressChangedEventArgs e)
{
int left = Console.CursorLeft;
int top = Console.CursorTop;
Console.Write(buffer);
Console.SetCursorPosition(left, top);
Console.WriteLine("Progress: {0}%", e.Progress);
Console.SetCursorPosition(left, top);
}
}
}

View File

@ -137,6 +137,19 @@ files, or remove any of them.
To pack the .ACB file back, you gotta have the extracted directory
and the .ACB file in the same directory, and also the external .AWB
file if it exists. Drag and drop the directory to the .EXE file,
it will collect all the files inside directory and pack them back.</value>
it will collect all the files inside directory and pack them back.
Settings (.exe.config)
=======================
BufferSize: Buffer size used to load/save data from files. Higher
values may make the progress faster or slower.
EnableThreading: Determines whether to use multiple threads to
extract data from files.
MaxThreads: Maximum amount of threads to extract data from files.
Higher values may make the progress faster or slower.
^ Go up.</value>
</data>
</root>

View File

@ -0,0 +1,62 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace AcbEditor.Properties {
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "15.1.0.0")]
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
public static Settings Default {
get {
return defaultInstance;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("True")]
public bool EnableThreading {
get {
return ((bool)(this["EnableThreading"]));
}
set {
this["EnableThreading"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("4")]
public int MaxThreads {
get {
return ((int)(this["MaxThreads"]));
}
set {
this["MaxThreads"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("4096")]
public int BufferSize {
get {
return ((int)(this["BufferSize"]));
}
set {
this["BufferSize"] = value;
}
}
}
}

View File

@ -0,0 +1,15 @@
<?xml version='1.0' encoding='utf-8'?>
<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)" GeneratedClassNamespace="AcbEditor.Properties" GeneratedClassName="Settings">
<Profiles />
<Settings>
<Setting Name="EnableThreading" Type="System.Boolean" Scope="User">
<Value Profile="(Default)">True</Value>
</Setting>
<Setting Name="MaxThreads" Type="System.Int32" Scope="User">
<Value Profile="(Default)">4</Value>
</Setting>
<Setting Name="BufferSize" Type="System.Int32" Scope="User">
<Value Profile="(Default)">4096</Value>
</Setting>
</Settings>
</SettingsFile>

View File

@ -1,18 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<sectionGroup name="userSettings" type="System.Configuration.UserSettingsGroup, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" >
<section name="CsbBuilder.Properties.Settings" type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" allowExeDefinition="MachineToLocalUser" requirePermission="false" />
</sectionGroup>
</configSections>
<startup useLegacyV2RuntimeActivationPolicy="true">
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2" />
</startup>
<userSettings>
<CsbBuilder.Properties.Settings>
<setting name="NameNodeAfterItsParent" serializeAs="String">
<value>False</value>
</setting>
</CsbBuilder.Properties.Settings>
</userSettings>
</configuration>

View File

@ -224,6 +224,8 @@
</ItemGroup>
<ItemGroup>
<Content Include="NAudio.LICENSE.txt" />
<None Include="Resources\Redo_grey_16x.png" />
<None Include="Resources\Undo_grey_16x.png" />
<None Include="Resources\Settings_16x.png" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />

View File

@ -32,7 +32,7 @@ namespace CsbBuilder
private void CopyReport(object sender, EventArgs e)
{
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.AppendLine($"Application: {Program.ApplicationVersion}");
stringBuilder.AppendLine($"Application: {Program.ApplicationTitle}");
stringBuilder.AppendLine($"Exception Message: {exception.Message}");
stringBuilder.AppendLine($"Exception Details:");
stringBuilder.AppendLine(exception.StackTrace);

View File

@ -22,6 +22,11 @@ namespace CsbBuilder.Importer
{
public static void Import(string path, CsbProject project)
{
var extractor = new DataExtractor();
extractor.BufferSize = MainForm.Settings.BufferSize;
extractor.EnableThreading = MainForm.Settings.EnableThreading;
extractor.MaxThreads = MainForm.Settings.MaxCores;
// Find the CPK first
string cpkPath = Path.ChangeExtension(path, "cpk");
bool exists = File.Exists(cpkPath);
@ -74,27 +79,27 @@ namespace CsbBuilder.Importer
soundElementNode.ChannelCount = soundElementTable.NumberChannels;
soundElementNode.SampleRate = soundElementTable.SoundFrequency;
soundElementNode.Streaming = soundElementTable.Streaming;
soundElementNode.SampleCount = soundElementTable.NumberSamples;
CriAaxArchive aaxArchive = new CriAaxArchive();
byte[] aaxData = soundElementTable.Data;
if (exists && soundElementNode.Streaming)
CriCpkEntry cpkEntry = null;
if (soundElementNode.Streaming)
{
using (Stream source = File.OpenRead(cpkPath))
using (Stream entrySource = cpkArchive.GetByPath(soundElementTable.Name).Open(source))
using (Stream entrySource = (cpkEntry = cpkArchive.GetByPath(soundElementTable.Name)).Open(source))
{
aaxData = ((Substream)entrySource).ToArray();
aaxArchive.Read(entrySource);
}
}
aaxArchive.Load(aaxData);
else
{
aaxArchive.Load(soundElementTable.Data);
}
foreach (CriAaxEntry entry in aaxArchive)
{
byte[] data = new byte[entry.Length];
Array.Copy(aaxData, entry.Position, data, 0, data.Length);
string outputFileName = Path.Combine(project.AudioDirectory.FullName, soundElementTable.Name.Replace('/', '_'));
if (entry.Flag == CriAaxEntryFlag.Intro)
{
@ -108,10 +113,15 @@ namespace CsbBuilder.Importer
soundElementNode.Loop = Path.GetFileName(outputFileName);
}
File.WriteAllBytes(outputFileName, data);
if (soundElementNode.Streaming)
{
extractor.Add(cpkPath, outputFileName, cpkEntry.Position + entry.Position, entry.Length);
}
// Read the samples just in case
soundElementNode.SampleCount += AdxFileReader.LoadHeader(outputFileName).SampleCount;
else
{
extractor.Add(soundElementTable.Data, outputFileName, entry.Position, entry.Length);
}
}
project.SoundElementNodes.Add(soundElementNode);
@ -300,6 +310,9 @@ namespace CsbBuilder.Importer
synthNode.VoiceLimitGroupReference = synthTable.VoiceLimitGroupName;
}
}
// Extract everything
extractor.Run();
}
}
}

View File

@ -385,7 +385,7 @@
this.aAXToolStripMenuItem});
this.toolStripMenuItem49.Image = global::CsbBuilder.Properties.Resources.Sound;
this.toolStripMenuItem49.Name = "toolStripMenuItem49";
this.toolStripMenuItem49.Size = new System.Drawing.Size(152, 22);
this.toolStripMenuItem49.Size = new System.Drawing.Size(138, 22);
this.toolStripMenuItem49.Text = "Audio Tools";
//
// aDXToolStripMenuItem
@ -393,7 +393,7 @@
this.aDXToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.convertADXsToWAVToolStripMenuItem});
this.aDXToolStripMenuItem.Name = "aDXToolStripMenuItem";
this.aDXToolStripMenuItem.Size = new System.Drawing.Size(152, 22);
this.aDXToolStripMenuItem.Size = new System.Drawing.Size(97, 22);
this.aDXToolStripMenuItem.Text = "ADX";
//
// convertADXsToWAVToolStripMenuItem
@ -409,33 +409,33 @@
this.extractAAXToFolderToolStripMenuItem,
this.packFolderToAAXToolStripMenuItem});
this.aAXToolStripMenuItem.Name = "aAXToolStripMenuItem";
this.aAXToolStripMenuItem.Size = new System.Drawing.Size(152, 22);
this.aAXToolStripMenuItem.Size = new System.Drawing.Size(97, 22);
this.aAXToolStripMenuItem.Text = "AAX";
//
// extractAAXToFolderToolStripMenuItem
//
this.extractAAXToFolderToolStripMenuItem.Name = "extractAAXToFolderToolStripMenuItem";
this.extractAAXToFolderToolStripMenuItem.Size = new System.Drawing.Size(198, 22);
this.extractAAXToFolderToolStripMenuItem.Size = new System.Drawing.Size(196, 22);
this.extractAAXToFolderToolStripMenuItem.Text = "Extract AAX(s) to folder";
this.extractAAXToFolderToolStripMenuItem.Click += new System.EventHandler(this.extractAAXToFolderToolStripMenuItem_Click);
//
// packFolderToAAXToolStripMenuItem
//
this.packFolderToAAXToolStripMenuItem.Name = "packFolderToAAXToolStripMenuItem";
this.packFolderToAAXToolStripMenuItem.Size = new System.Drawing.Size(198, 22);
this.packFolderToAAXToolStripMenuItem.Size = new System.Drawing.Size(196, 22);
this.packFolderToAAXToolStripMenuItem.Text = "Pack ADX(s) to AAX";
this.packFolderToAAXToolStripMenuItem.Click += new System.EventHandler(this.packFolderToAAXToolStripMenuItem_Click);
//
// toolStripSeparator26
//
this.toolStripSeparator26.Name = "toolStripSeparator26";
this.toolStripSeparator26.Size = new System.Drawing.Size(149, 6);
this.toolStripSeparator26.Size = new System.Drawing.Size(135, 6);
//
// settingsButton
//
this.settingsButton.Image = global::CsbBuilder.Properties.Resources.Settings;
this.settingsButton.Name = "settingsButton";
this.settingsButton.Size = new System.Drawing.Size(152, 22);
this.settingsButton.Size = new System.Drawing.Size(138, 22);
this.settingsButton.Text = "Settings";
this.settingsButton.Click += new System.EventHandler(this.OpenSettings);
//

View File

@ -100,7 +100,7 @@ namespace CsbBuilder
{
project = null;
ClearTreeViews();
Text = "CSB Builder";
Text = Program.ApplicationTitle;
saved = true;
}
@ -1918,7 +1918,7 @@ namespace CsbBuilder
private void ShowAbout(object sender, EventArgs e)
{
MessageBox.Show($"{Program.ApplicationVersion} by Skyth (blueskythlikesclouds)", "CSB Builder");
MessageBox.Show($"{Program.ApplicationTitle} by Skyth (blueskythlikesclouds)", "CSB Builder");
}
private void CreateChildTrackNode(object sender, EventArgs e)

View File

@ -22,12 +22,12 @@ namespace CsbBuilder
}
}
public static string ApplicationVersion
public static string ApplicationTitle
{
get
{
AssemblyName assemblyName = Assembly.GetEntryAssembly().GetName();
return $"{assemblyName.Name} (Version {assemblyName.Version.ToString()})";
return $"CSB Builder {assemblyName.Version.ToString()}";
}
}

View File

@ -200,6 +200,16 @@ namespace CsbBuilder.Properties {
}
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Bitmap.
/// </summary>
internal static System.Drawing.Bitmap Redo {
get {
object obj = ResourceManager.GetObject("Redo", resourceCulture);
return ((System.Drawing.Bitmap)(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Bitmap.
/// </summary>
@ -289,5 +299,15 @@ namespace CsbBuilder.Properties {
return ((System.Drawing.Bitmap)(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Bitmap.
/// </summary>
internal static System.Drawing.Bitmap Undo {
get {
object obj = ResourceManager.GetObject("Undo", resourceCulture);
return ((System.Drawing.Bitmap)(obj));
}
}
}
}

View File

@ -187,4 +187,10 @@
<data name="Settings" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\Settings_16x.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="Redo" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\Redo_grey_16x.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="Undo" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\Undo_grey_16x.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
</root>

Binary file not shown.

After

Width:  |  Height:  |  Size: 416 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 B

View File

@ -42,14 +42,22 @@ namespace CsbBuilder.Project
[DisplayName("Rename Sound node to referenced Sound Element node"), Category("Application")]
public bool RenameToSoundElement { get; set; }
[DisplayName("Enable threading"), Category("Stream")]
[Description("Determines whether to use threads to extract data from CSB/CPK files during importing. It may make the importing faster or slower.")]
public bool EnableThreading { get; set; }
[DisplayName("Maximum amount of cores"), Category("Stream")]
[Description("Maximum amount of threads used to extract data from CSB/CPK files during importing.")]
public int MaxCores { get; set; }
public static Settings Load()
{
string path = Path.ChangeExtension(Application.ExecutablePath, "xml");
Settings settings = null;
if (File.Exists(path))
{
if (File.Exists(path))
{
XmlSerializer serializer = new XmlSerializer(typeof(Settings));
using (Stream source = File.OpenRead(path))
@ -90,6 +98,8 @@ namespace CsbBuilder.Project
ProjectsName = "New CSB Project";
ImportedCsbProjectDirectory = ProjectDirectory.DirectoryOfCsb;
RenameToSoundElement = true;
EnableThreading = true;
MaxCores = 4;
}
}
}

View File

@ -1,6 +1,24 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2" />
</startup>
<configSections>
<sectionGroup name="userSettings" type="System.Configuration.UserSettingsGroup, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" >
<section name="CsbEditor.Properties.Settings" type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" allowExeDefinition="MachineToLocalUser" requirePermission="false" />
</sectionGroup>
</configSections>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2" />
</startup>
<userSettings>
<CsbEditor.Properties.Settings>
<setting name="EnableThreading" serializeAs="String">
<value>True</value>
</setting>
<setting name="MaxThreads" serializeAs="String">
<value>4</value>
</setting>
<setting name="BufferSize" serializeAs="String">
<value>4096</value>
</setting>
</CsbEditor.Properties.Settings>
</userSettings>
</configuration>

View File

@ -51,9 +51,19 @@
<DesignTime>True</DesignTime>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<Compile Include="Properties\Settings.Designer.cs">
<AutoGen>True</AutoGen>
<DesignTimeSharedInput>True</DesignTimeSharedInput>
<DependentUpon>Settings.settings</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
<None Include="Properties\Settings.settings">
<SubType>Designer</SubType>
<Generator>SettingsSingleFileGenerator</Generator>
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
</None>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SonicAudioLib\SonicAudioLib.csproj">

View File

@ -4,6 +4,9 @@ using System.IO;
using System.Windows.Forms;
using System.Collections.Generic;
using CsbEditor.Properties;
using SonicAudioLib;
using SonicAudioLib.CriMw;
using SonicAudioLib.IO;
using SonicAudioLib.Archive;
@ -18,7 +21,7 @@ namespace CsbEditor
{
if (args.Length < 1)
{
Console.WriteLine(Properties.Resources.Description);
Console.WriteLine(Resources.Description);
Console.ReadLine();
return;
}
@ -29,6 +32,13 @@ namespace CsbEditor
#endif
if (args[0].EndsWith(".csb"))
{
var extractor = new DataExtractor();
extractor.ProgressChanged += OnProgressChanged;
extractor.BufferSize = Settings.Default.BufferSize;
extractor.EnableThreading = Settings.Default.EnableThreading;
extractor.MaxThreads = Settings.Default.MaxThreads;
string baseDirectory = Path.GetDirectoryName(args[0]);
string outputDirectoryName = Path.Combine(baseDirectory, Path.GetFileNameWithoutExtension(args[0]));
@ -42,6 +52,7 @@ namespace CsbEditor
{
if (reader.GetString("name") == "SOUND_ELEMENT")
{
long tablePosition = reader.GetPosition("utf");
using (CriTableReader sdlReader = CriTableReader.Create(reader.GetSubstream("utf")))
{
while (sdlReader.Read())
@ -60,15 +71,13 @@ namespace CsbEditor
else if (streaming && found && cpkArchive == null)
{
cpkArchive = new CriCpkArchive();
cpkArchive.Load(cpkPath);
cpkArchive.Load(cpkPath, Settings.Default.BufferSize);
}
string sdlName = sdlReader.GetString("name");
DirectoryInfo destinationPath = new DirectoryInfo(Path.Combine(outputDirectoryName, sdlName));
destinationPath.Create();
Console.WriteLine("Extracting {0}...", sdlName);
CriAaxArchive aaxArchive = new CriAaxArchive();
if (streaming)
@ -84,12 +93,10 @@ namespace CsbEditor
foreach (CriAaxEntry entry in aaxArchive)
{
using (Stream destination = File.Create(Path.Combine(destinationPath.FullName,
entry.Flag == CriAaxEntryFlag.Intro ? "Intro.adx" : "Loop.adx")))
using (Stream entrySource = entry.Open(aaxSource))
{
entrySource.CopyTo(destination);
}
extractor.Add(cpkPath,
Path.Combine(destinationPath.FullName,
entry.Flag == CriAaxEntryFlag.Intro ? "Intro.adx" : "Loop.adx"),
cpkEntry.Position + entry.Position, entry.Length);
}
}
}
@ -97,18 +104,17 @@ namespace CsbEditor
else
{
long aaxPosition = sdlReader.GetPosition("data");
using (Stream aaxSource = sdlReader.GetSubstream("data"))
{
aaxArchive.Read(aaxSource);
foreach (CriAaxEntry entry in aaxArchive)
{
using (Stream destination = File.Create(Path.Combine(destinationPath.FullName,
entry.Flag == CriAaxEntryFlag.Intro ? "Intro.adx" : "Loop.adx")))
using (Stream entrySource = entry.Open(aaxSource))
{
entrySource.CopyTo(destination);
}
extractor.Add(args[0],
Path.Combine(destinationPath.FullName,
entry.Flag == CriAaxEntryFlag.Intro ? "Intro.adx" : "Loop.adx"),
tablePosition + aaxPosition + entry.Position, entry.Length);
}
}
}
@ -119,6 +125,8 @@ namespace CsbEditor
}
}
}
extractor.Run();
}
else if (File.GetAttributes(args[0]).HasFlag(FileAttributes.Directory))
@ -132,9 +140,10 @@ namespace CsbEditor
}
CriCpkArchive cpkArchive = new CriCpkArchive();
cpkArchive.ProgressChanged += OnProgressChanged;
CriTable csbFile = new CriTable();
csbFile.Load(csbPath);
csbFile.Load(csbPath, Settings.Default.BufferSize);
CriRow soundElementRow = csbFile.Rows.First(row => (string)row["name"] == "SOUND_ELEMENT");
@ -158,8 +167,6 @@ namespace CsbEditor
uint sampleRate = (uint)sdlRow["sfreq"];
byte numberChannels = (byte)sdlRow["nch"];
Console.WriteLine("Adding {0}...", sdlName);
CriAaxArchive aaxArchive = new CriAaxArchive();
foreach (FileInfo file in sdlDirectory.GetFiles("*.adx"))
{
@ -193,7 +200,7 @@ namespace CsbEditor
junks.Add(entry.FilePath);
cpkArchive.Add(entry);
aaxArchive.Save(entry.FilePath.FullName);
aaxArchive.Save(entry.FilePath.FullName, Settings.Default.BufferSize);
}
else
@ -209,11 +216,11 @@ namespace CsbEditor
soundElementRow["utf"] = soundElementTable.Save();
csbFile.WriterSettings = CriTableWriterSettings.AdxSettings;
csbFile.Save(args[0] + ".csb");
csbFile.Save(args[0] + ".csb", Settings.Default.BufferSize);
if (cpkArchive.Count > 0)
{
cpkArchive.Save(args[0] + ".cpk");
cpkArchive.Save(args[0] + ".cpk", Settings.Default.BufferSize);
}
foreach (FileInfo junk in junks)
@ -240,5 +247,18 @@ namespace CsbEditor
sampleRate = EndianStream.ReadUInt32BE(source);
}
}
private static string buffer = new string(' ', 17);
private static void OnProgressChanged(object sender, ProgressChangedEventArgs e)
{
int left = Console.CursorLeft;
int top = Console.CursorTop;
Console.Write(buffer);
Console.SetCursorPosition(left, top);
Console.WriteLine("Progress: {0}%", e.Progress);
Console.SetCursorPosition(left, top);
}
}
}

View File

@ -139,6 +139,19 @@ rate or channel count than original.
To pack the .CSB file back, you gotta have the extracted directory
and the .CSB file in the same directory. Drag and drop the directory
to the .EXE file, it will collect all the files inside directory
and pack them back.</value>
and pack them back.
Settings (.exe.config)
=======================
BufferSize: Buffer size used to load/save data from files. Higher
values may make the progress faster or slower.
EnableThreading: Determines whether to use multiple threads to
extract data from files.
MaxThreads: Maximum amount of threads to extract data from files.
Higher values may make the progress faster or slower.
^ Go up.</value>
</data>
</root>

View File

@ -0,0 +1,62 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace CsbEditor.Properties {
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "15.1.0.0")]
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
public static Settings Default {
get {
return defaultInstance;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("True")]
public bool EnableThreading {
get {
return ((bool)(this["EnableThreading"]));
}
set {
this["EnableThreading"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("4")]
public int MaxThreads {
get {
return ((int)(this["MaxThreads"]));
}
set {
this["MaxThreads"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("4096")]
public int BufferSize {
get {
return ((int)(this["BufferSize"]));
}
set {
this["BufferSize"] = value;
}
}
}
}

View File

@ -0,0 +1,15 @@
<?xml version='1.0' encoding='utf-8'?>
<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)" GeneratedClassNamespace="CsbEditor.Properties" GeneratedClassName="Settings">
<Profiles />
<Settings>
<Setting Name="EnableThreading" Type="System.Boolean" Scope="User">
<Value Profile="(Default)">True</Value>
</Setting>
<Setting Name="MaxThreads" Type="System.Int32" Scope="User">
<Value Profile="(Default)">4</Value>
</Setting>
<Setting Name="BufferSize" Type="System.Int32" Scope="User">
<Value Profile="(Default)">4096</Value>
</Setting>
</Settings>
</SettingsFile>

View File

@ -1,16 +1,16 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.IO.Compression;
using System.ComponentModel;
using System.Collections;
using SonicAudioLib;
using SonicAudioLib.Archive;
using SonicAudioLib.IO;
using SonicAudioLib.CriMw;
using System.Threading.Tasks;
using System.Xml;
@ -22,19 +22,26 @@ namespace SonicAudioCmd
{
try
{
var extractor = new DataExtractor();
extractor.ProgressChanged += OnProgressChanged;
CriCpkArchive archive = new CriCpkArchive();
archive.Load(args[0]);
using (Stream source = File.OpenRead(args[0]))
foreach (CriCpkEntry entry in archive)
{
foreach (CriCpkEntry entry in archive)
{
using (Stream destination = File.Create(entry.Name))
{
EndianStream.CopyPartTo(source, destination, entry.Position, entry.Length, ushort.MaxValue);
}
}
extractor.Add(args[0], Path.Combine(Path.ChangeExtension(args[0], null), entry.DirectoryName, entry.Name), entry.Position, entry.Length, entry.IsCompressed);
}
DateTime dateTime = DateTime.Now;
extractor.Run();
Console.WriteLine("Elapsed time: {0}", DateTime.Now - dateTime);
Console.ReadLine();
/*archive.EnableMask = true;
archive.Save("test.cpk");*/
}
catch (Exception exception)
@ -43,5 +50,18 @@ namespace SonicAudioCmd
Console.ReadLine();
}
}
private static string buffer = new string(' ', 17);
private static void OnProgressChanged(object sender, ProgressChangedEventArgs e)
{
int left = Console.CursorLeft;
int top = Console.CursorTop;
Console.Write(buffer);
Console.SetCursorPosition(left, top);
Console.WriteLine("Progress: {0}%", e.Progress);
Console.SetCursorPosition(left, top);
}
}
}

View File

@ -52,6 +52,8 @@ namespace SonicAudioLib.Archive
{
protected List<T> entries = new List<T>();
public virtual event ProgressChanged ProgressChanged;
public virtual T this[int index]
{
get
@ -108,6 +110,11 @@ namespace SonicAudioLib.Archive
return entries.GetEnumerator();
}
protected void OnProgressChanged(object sender, ProgressChangedEventArgs e)
{
ProgressChanged?.Invoke(this, e);
}
#if DEBUG
public void Print()
{

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Diagnostics;
using SonicAudioLib.IO;
using SonicAudioLib.Module;
@ -11,17 +12,39 @@ namespace SonicAudioLib.Archive
{
public class CriAfs2Entry : EntryBase
{
public ushort Id { get; set; }
public uint Id { get; set; }
}
public class CriAfs2Archive : ArchiveBase<CriAfs2Entry>
{
public uint Align { get; set; }
public uint IdFieldLength { get; set; }
public uint PositionFieldLength { get; set; }
/// <summary>
/// Gets header of the written AFS2 archive.
/// Should be gotten after <see cref="Write(Stream)"/> is called.
/// </summary>
public byte[] Header { get; private set; }
public override void Read(Stream source)
{
long ReadByLength(uint length)
{
switch (length)
{
case 2:
return EndianStream.ReadUInt16(source);
case 4:
return EndianStream.ReadUInt32(source);
case 8:
return EndianStream.ReadInt64(source);
default:
throw new ArgumentException($"Unimplemented field length ({length})", nameof(length));
}
}
if (EndianStream.ReadCString(source, 4) != "AFS2")
{
throw new InvalidDataException("'AFS2' signature could not be found.");
@ -35,10 +58,10 @@ namespace SonicAudioLib.Archive
throw new InvalidDataException($"Unknown AFS2 type ({type}). Please report this error with the file(s).");
}
IdFieldLength = (information >> 16) & 0xFF;
PositionFieldLength = (information >> 8) & 0xFF;
uint idFieldLength = (information >> 16) & 0xFF;
uint positionFieldLength = (information >> 8) & 0xFF;
ushort entryCount = (ushort)EndianStream.ReadUInt32(source);
uint entryCount = EndianStream.ReadUInt32(source);
Align = EndianStream.ReadUInt32(source);
CriAfs2Entry previousEntry = null;
@ -46,35 +69,13 @@ namespace SonicAudioLib.Archive
{
CriAfs2Entry afs2Entry = new CriAfs2Entry();
long idPosition = 16 + (i * IdFieldLength);
long idPosition = 16 + (i * idFieldLength);
source.Seek(idPosition, SeekOrigin.Begin);
afs2Entry.Id = (uint)ReadByLength(idFieldLength);
switch (IdFieldLength)
{
case 2:
afs2Entry.Id = EndianStream.ReadUInt16(source);
break;
default:
throw new InvalidDataException($"Unknown IdFieldLength ({IdFieldLength}). Please report this error with the file(s).");
}
long positionPosition = 16 + (entryCount * IdFieldLength) + (i * PositionFieldLength);
long positionPosition = 16 + (entryCount * idFieldLength) + (i * positionFieldLength);
source.Seek(positionPosition, SeekOrigin.Begin);
switch (PositionFieldLength)
{
case 2:
afs2Entry.Position = EndianStream.ReadUInt16(source);
break;
case 4:
afs2Entry.Position = EndianStream.ReadUInt32(source);
break;
default:
throw new InvalidDataException($"Unknown PositionFieldLength ({PositionFieldLength}). Please report this error with the file(s).");
}
afs2Entry.Position = ReadByLength(positionFieldLength);
if (previousEntry != null)
{
@ -85,16 +86,7 @@ namespace SonicAudioLib.Archive
if (i == entryCount - 1)
{
switch (PositionFieldLength)
{
case 2:
afs2Entry.Length = EndianStream.ReadUInt16(source) - afs2Entry.Position;
break;
case 4:
afs2Entry.Length = EndianStream.ReadUInt32(source) - afs2Entry.Position;
break;
}
afs2Entry.Length = ReadByLength(positionFieldLength) - afs2Entry.Position;
}
entries.Add(afs2Entry);
@ -103,68 +95,108 @@ namespace SonicAudioLib.Archive
}
public override void Write(Stream destination)
{
uint headerLength = (uint)(16 + (entries.Count * IdFieldLength) + (entries.Count * PositionFieldLength) + PositionFieldLength);
{
long GetHeaderLength(uint idFieldLen, uint positionFieldLen)
{
return 16 + (idFieldLen * entries.Count) + (positionFieldLen * entries.Count) + positionFieldLen;
}
EndianStream.WriteCString(destination, "AFS2", 4);
EndianStream.WriteUInt32(destination, 1 | (IdFieldLength << 16) | (PositionFieldLength << 8));
EndianStream.WriteUInt32(destination, (ushort)entries.Count);
EndianStream.WriteUInt32(destination, Align);
long Calculate(out uint idFieldLen, out uint positionFieldLen)
{
// It's kind of impossible to have more than 65535 waveforms in one ACB, but just to make sure.
idFieldLen = entries.Count <= ushort.MaxValue ? 2u : 4u;
long dataLength = 0;
foreach (CriAfs2Entry entry in entries)
{
dataLength = Helpers.Align(dataLength, Align);
dataLength += entry.Length;
}
positionFieldLen = 2;
long headerLen;
// Check 2, 4 and 8
if (Helpers.Align((headerLen = GetHeaderLength(idFieldLen, positionFieldLen)), Align) + dataLength > ushort.MaxValue)
{
positionFieldLen = 4;
if (Helpers.Align((headerLen = GetHeaderLength(idFieldLen, positionFieldLen)), Align) + dataLength > uint.MaxValue)
{
positionFieldLen = 8;
}
}
return headerLen;
}
var mDestination = new MemoryStream();
void WriteByLength(uint length, long value)
{
switch (length)
{
case 2:
EndianStream.WriteUInt16(mDestination, (ushort)value);
break;
case 4:
EndianStream.WriteUInt32(mDestination, (uint)value);
break;
case 8:
EndianStream.WriteInt64(mDestination, value);
break;
default:
throw new ArgumentException($"Unimplemented field length ({length})", nameof(length));
}
}
long headerLength = Calculate(out uint idFieldLength, out uint positionFieldLength);
EndianStream.WriteCString(mDestination, "AFS2", 4);
EndianStream.WriteUInt32(mDestination, 1 | (idFieldLength << 16) | (positionFieldLength << 8));
EndianStream.WriteUInt32(mDestination, (uint)entries.Count);
EndianStream.WriteUInt32(mDestination, Align);
VldPool vldPool = new VldPool(Align, headerLength);
vldPool.ProgressChanged += OnProgressChanged;
var orderedEntries = entries.OrderBy(entry => entry.Id);
foreach (CriAfs2Entry afs2Entry in orderedEntries)
{
switch (IdFieldLength)
{
case 2:
EndianStream.WriteUInt16(destination, (ushort)afs2Entry.Id);
break;
default:
throw new NotImplementedException($"Unimplemented IdFieldLength ({IdFieldLength}). Implemented length: (2)");
}
WriteByLength(idFieldLength, afs2Entry.Id);
}
foreach (CriAfs2Entry afs2Entry in orderedEntries)
{
uint entryPosition = (uint)vldPool.Length;
long entryPosition = vldPool.Length;
vldPool.Put(afs2Entry.FilePath);
switch (PositionFieldLength)
{
case 2:
EndianStream.WriteUInt16(destination, (ushort)entryPosition);
break;
case 4:
EndianStream.WriteUInt32(destination, entryPosition);
break;
default:
throw new InvalidDataException($"Unimplemented PositionFieldLength ({PositionFieldLength}). Implemented lengths: (2, 4)");
}
afs2Entry.Position = entryPosition;
WriteByLength(positionFieldLength, entryPosition);
}
EndianStream.WriteUInt32(destination, (uint)vldPool.Length);
WriteByLength(positionFieldLength, vldPool.Length);
// Copy the header to Header property and save it to destination
Header = mDestination.ToArray();
mDestination.Close();
destination.Write(Header, 0, Header.Length);
vldPool.Write(destination);
vldPool.Clear();
}
public CriAfs2Entry GetById(uint cueIndex)
public CriAfs2Entry GetById(uint id)
{
return entries.FirstOrDefault(e => (e.Id == cueIndex));
return entries.FirstOrDefault(e => (e.Id == id));
}
public CriAfs2Archive()
{
Align = 32;
IdFieldLength = 2;
PositionFieldLength = 4;
}
}
}

View File

@ -104,7 +104,7 @@ namespace SonicAudioLib.Archive
public override void Read(Stream source)
{
using (CriTableReader reader = CriCpkSection.Open(source, source.Position))
using (CriTableReader reader = CriCpkSection.Open(source, source.Position, "CPK "))
{
reader.Read();
@ -156,8 +156,8 @@ namespace SonicAudioLib.Archive
if (mode == CriCpkMode.FileName || mode == CriCpkMode.FileNameAndId)
{
using (CriTableReader tocReader = CriCpkSection.Open(source, tocPosition))
using (CriTableReader etocReader = CriCpkSection.Open(source, etocPosition))
using (CriTableReader tocReader = CriCpkSection.Open(source, tocPosition, "TOC "))
using (CriTableReader etocReader = CriCpkSection.Open(source, etocPosition, "ETOC"))
{
while (tocReader.Read())
{
@ -191,7 +191,7 @@ namespace SonicAudioLib.Archive
if (mode == CriCpkMode.FileNameAndId && isLatestVersion)
{
using (CriTableReader itocReader = CriCpkSection.Open(source, itocPosition))
using (CriTableReader itocReader = CriCpkSection.Open(source, itocPosition, "ITOC"))
{
while (itocReader.Read())
{
@ -203,7 +203,7 @@ namespace SonicAudioLib.Archive
else if (mode == CriCpkMode.Id)
{
using (CriTableReader itocReader = CriCpkSection.Open(source, itocPosition))
using (CriTableReader itocReader = CriCpkSection.Open(source, itocPosition, "ITOC"))
{
while (itocReader.Read())
{
@ -269,6 +269,7 @@ namespace SonicAudioLib.Archive
}
VldPool vldPool = new VldPool(Align, 2048);
vldPool.ProgressChanged += OnProgressChanged;
using (CriCpkSection cpkSection = new CriCpkSection(destination, "CPK ", enableMask))
{
@ -391,7 +392,7 @@ namespace SonicAudioLib.Archive
etocSection.Writer.WriteRow(true,
CpkDateTimeFromDateTime(entry.UpdateDateTime),
entry.FilePath.DirectoryName.Replace('\\', '/'));
entry.DirectoryName);
vldPool.Put(entry.FilePath);
}
@ -586,27 +587,121 @@ namespace SonicAudioLib.Archive
});
}
private DateTime DateTimeFromCpkDateTime(ulong dateTime)
public static void Decompress(Stream source, long position, Stream destination)
{
if (dateTime == 0)
source.Position = position;
if (EndianStream.ReadCString(source, 8) != "CRILAYLA")
{
return new DateTime();
throw new InvalidDataException("'CRILAYLA' signature could not be found.");
}
return new DateTime(
(int)(uint)(dateTime >> 32) >> 16, // year
(int)(uint)(dateTime >> 32) >> 8 & byte.MaxValue, // month
(int)(uint)(dateTime >> 32) & byte.MaxValue, // day
(int)(uint)dateTime >> 24, // hour
(int)(uint)dateTime >> 16 & byte.MaxValue, // minute
(int)(uint)dateTime >> 8 & byte.MaxValue // second
);
uint uncompressedLength = EndianStream.ReadUInt32(source);
uint compressedLength = EndianStream.ReadUInt32(source);
// Copy the uncompressed header to destination
EndianStream.CopyPartTo(source, destination, position + 0x10 + compressedLength, 0x100, 4096);
// Bit methods
int bitCount = 0;
int bitData = 0;
int ReadBits(int count)
{
if (bitCount < count)
{
int neededBytes = ((24 - bitCount) >> 3) + 1;
bitCount += neededBytes * 8;
while (neededBytes > 0)
{
bitData = (bitData << 8) | source.ReadByte();
neededBytes--;
source.Position -= 2;
}
}
bitCount -= count;
return (bitData >> bitCount) & ((1 << count) - 1);
}
// Deflate levels
IEnumerable<int> GetDeflateLevels()
{
yield return 2;
yield return 3;
yield return 5;
while (true)
{
yield return 8;
}
}
// Decompression
source.Position = position + 0x10 + compressedLength - 1;
long writtenBytes = 0;
byte[] buffer = new byte[uncompressedLength];
while (writtenBytes < uncompressedLength)
{
// Verbatim byte, directly copy
if (ReadBits(1) == 0)
{
buffer[uncompressedLength - (writtenBytes++) - 1] = (byte)ReadBits(8);
}
// Referenced bytes
else
{
long pos = uncompressedLength - 1 - writtenBytes + ReadBits(13) + 3;
long length = 3;
foreach (var deflateLevel in GetDeflateLevels())
{
long len = ReadBits(deflateLevel);
length += len;
if (len != ((1 << deflateLevel) - 1))
{
break;
}
}
while (length > 0)
{
buffer[uncompressedLength - 1 - (writtenBytes++)] = buffer[pos--];
length--;
}
}
}
destination.Write(buffer, 0, buffer.Length);
}
private DateTime DateTimeFromCpkDateTime(ulong dateTime)
{
return dateTime > 0 ?
new DateTime(
(int)(dateTime >> 48), // Year (ushort)
(int)(dateTime >> 40) & 0xFF, // Month (byte)
(int)(dateTime >> 32) & 0xFF, // Day (byte)
(int)(dateTime >> 24) & 0xFF, // Hour (byte)
(int)(dateTime >> 16) & 0xFF, // Minute (byte)
(int)(dateTime >> 8) & 0xFF) // Second (byte)
: new DateTime();
}
private ulong CpkDateTimeFromDateTime(DateTime dateTime)
{
return ((((ulong)dateTime.Year * 0x100 + (uint)dateTime.Month) * 0x100 + (uint)dateTime.Day) * 0x100000000) +
((((ulong)dateTime.Hour * 0x100 + (uint)dateTime.Minute) * 0x100 + (uint)dateTime.Second) * 0x100);
return
(ulong)dateTime.Year << 48 |
(ulong)dateTime.Month << 40 |
(ulong)dateTime.Day << 32 |
(ulong)dateTime.Hour << 24 |
(ulong)dateTime.Minute << 16 |
(ulong)dateTime.Second << 8;
}
private class CriCpkSection : IDisposable
@ -637,16 +732,19 @@ namespace SonicAudioLib.Archive
destination.Seek(position, SeekOrigin.Begin);
}
public static CriTableReader Open(Stream source, long position)
public static CriTableReader Open(Stream source, long position, string expectedSignature)
{
source.Seek(position, SeekOrigin.Begin);
string signature = EndianStream.ReadCString(source, 4);
if (EndianStream.ReadCString(source, 4) != expectedSignature)
{
throw new InvalidDataException($"'{expectedSignature}' signature could not be found.");
}
uint flag = EndianStream.ReadUInt32(source);
uint tableLength = EndianStream.ReadUInt32(source);
uint unknown = EndianStream.ReadUInt32(source);
return CriTableReader.Create(new Substream(source, source.Position, tableLength));
}
@ -656,10 +754,10 @@ namespace SonicAudioLib.Archive
headerPosition = destination.Position;
EndianStream.WriteCString(destination, signature, 4);
EndianStream.WriteUInt32(destination, byte.MaxValue);
EndianStream.WriteUInt32(destination, 0xFF);
destination.Seek(8, SeekOrigin.Current);
writer = CriTableWriter.Create(destination, new CriTableWriterSettings() { LeaveOpen = true, EnableMask = enableMask });
writer = CriTableWriter.Create(destination, new CriTableWriterSettings() { LeaveOpen = true, EnableMask = enableMask, MaskXor = 25951, MaskXorMultiplier = 16661 });
}
}
}

View File

@ -1,4 +1,5 @@
using System;
using System.IO;
namespace SonicAudioLib.CriMw
{
@ -52,4 +53,81 @@ namespace SonicAudioLib.CriMw
public uint Length;
public object Value;
}
static class CriTableMasker
{
public static void FindKeys(byte[] signature, out uint xor, out uint xorMultiplier)
{
for (byte x = 0; x <= byte.MaxValue; x++)
{
// Find XOR using first byte
if ((signature[0] ^ x) == CriTableHeader.Signature[0])
{
// Matched the first byte, try finding the multiplier with the second byte
for (byte m = 0; m <= byte.MaxValue; m++)
{
// Matched the second byte, now make sure the other bytes match as well
if ((signature[1] ^ (byte)(x * m)) == CriTableHeader.Signature[1])
{
byte _x = (byte)(x * m);
bool allMatches = true;
for (int i = 2; i < 4; i++)
{
_x *= m;
if ((signature[i] ^ _x) != CriTableHeader.Signature[i])
{
allMatches = false;
break;
}
}
// All matches, return the xor and multiplier
if (allMatches)
{
xor = x;
xorMultiplier = m;
return;
}
}
}
}
}
throw new InvalidDataException("'@UTF' signature could not be found.");
}
public static void Mask(Stream source, Stream destination, long length, uint xor, uint xorMultiplier)
{
uint currentXor = xor;
long currentPosition = source.Position;
while (source.Position < currentPosition + length)
{
byte maskedByte = (byte)(source.ReadByte() ^ currentXor);
currentXor *= xorMultiplier;
destination.WriteByte(maskedByte);
}
}
public static void Mask(Stream source, long length, uint xor, uint xorMultiplier)
{
if (source.CanRead && source.CanWrite)
{
uint currentXor = xor;
long currentPosition = source.Position;
while (source.Position < currentPosition + length)
{
byte maskedByte = (byte)(source.ReadByte() ^ currentXor);
currentXor *= xorMultiplier;
source.Position--;
source.WriteByte(maskedByte);
}
}
}
}
}

View File

@ -86,19 +86,13 @@ namespace SonicAudioLib.CriMw
if (EndianStream.ReadCString(source, 4) != CriTableHeader.Signature)
{
// try to decrypt (currently only for CPK files since those are the only examples I have)
source.Seek(headerPosition, SeekOrigin.Begin);
MemoryStream unmaskedSource = new MemoryStream();
Helpers.MaskCriTable(source, unmaskedSource, source.Length);
// try again
unmaskedSource.Seek(0, SeekOrigin.Begin);
source.Position = headerPosition;
CriTableMasker.FindKeys(EndianStream.ReadBytes(source, 4), out uint x, out uint m);
if (EndianStream.ReadCString(unmaskedSource, 4) != CriTableHeader.Signature)
{
throw new InvalidDataException("'@UTF' signature could not be found.");
}
source.Position = headerPosition;
CriTableMasker.Mask(source, unmaskedSource, source.Length, x, m);
// Close the old stream
if (!leaveOpen)
@ -107,6 +101,7 @@ namespace SonicAudioLib.CriMw
}
source = unmaskedSource;
source.Position = 4;
}
header.Length = ReadUInt32() + 0x8;
@ -143,7 +138,6 @@ namespace SonicAudioLib.CriMw
for (ushort i = 0; i < header.NumberOfFields; i++)
{
CriTableField field = new CriTableField();
field.Flag = (CriFieldFlag)ReadByte();
if (field.Flag.HasFlag(CriFieldFlag.Name))
@ -155,10 +149,7 @@ namespace SonicAudioLib.CriMw
{
if (field.Flag.HasFlag(CriFieldFlag.Data))
{
ReadData(out uint vldPosition, out uint vldLength);
field.Position = vldPosition;
field.Length = vldLength;
ReadData(out field.Position, out field.Length);
}
else
@ -525,7 +516,7 @@ namespace SonicAudioLib.CriMw
GoToValue(fieldIndex);
ReadData(out uint vldPosition, out uint vldLength);
return (uint)(headerPosition + header.DataPoolPosition + vldPosition);
return headerPosition + header.DataPoolPosition + vldPosition;
}
public uint GetPosition(string fieldName)
@ -623,7 +614,7 @@ namespace SonicAudioLib.CriMw
source.Position = previousPosition;
if (readString == "<NULL>" || (readString == header.TableName && stringPosition == 0))
if (readString == StringPool.AdxBlankString || (readString == header.TableName && stringPosition == 0))
{
return string.Empty;
}

View File

@ -137,7 +137,7 @@ namespace SonicAudioLib.CriMw
if (settings.EnableMask)
{
destination.Position = headerPosition;
Helpers.MaskCriTable(destination, header.Length);
CriTableMasker.Mask(destination, header.Length, settings.MaskXor, settings.MaskXorMultiplier);
}
destination.Seek(previousPosition, SeekOrigin.Begin);
@ -699,6 +699,9 @@ namespace SonicAudioLib.CriMw
}
}
public uint MaskXor { get; set; }
public uint MaskXorMultiplier { get; set; }
public static CriTableWriterSettings AdxSettings
{
get

View File

@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
namespace SonicAudioLib
{
public class Helpers
{
public static long Align(long value, long alignment)
{
while ((value % alignment) != 0)
{
value++;
}
return value;
}
}
}

View File

@ -0,0 +1,107 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using SonicAudioLib.Archive;
namespace SonicAudioLib.IO
{
public class DataExtractor
{
public enum LoggingType
{
Progress,
Message,
}
private class Item
{
public object Source { get; set; }
public string DestinationFileName { get; set; }
public long Position { get; set; }
public long Length { get; set; }
public bool DecompressFromCriLayla { get; set; }
}
private List<Item> items = new List<Item>();
public int BufferSize { get; set; }
public bool EnableThreading { get; set; }
public int MaxThreads { get; set; }
public event ProgressChanged ProgressChanged;
public void Add(object source, string destinationFileName, long position, long length, bool decompressFromCriLayla = false)
{
items.Add(new Item { Source = source, DestinationFileName = destinationFileName, Position = position, Length = length, DecompressFromCriLayla = decompressFromCriLayla });
}
public void Run()
{
double progress = 0.0;
double factor = 100.0 / items.Count;
object lockTarget = new object();
Action<Item> action = item =>
{
if (ProgressChanged != null)
{
lock (lockTarget)
{
progress += factor;
ProgressChanged(this, new ProgressChangedEventArgs(progress));
}
}
FileInfo destinationFileName = new FileInfo(item.DestinationFileName);
if (!destinationFileName.Directory.Exists)
{
destinationFileName.Directory.Create();
}
using (Stream source =
item.Source is string fileName ? new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read, BufferSize) :
item.Source is byte[] byteArray ? new MemoryStream(byteArray) :
item.Source is Stream stream ? stream :
throw new ArgumentException("Unknown source in item", nameof(item.Source))
)
using (Stream destination = destinationFileName.Create())
{
if (item.DecompressFromCriLayla)
{
CriCpkArchive.Decompress(source, item.Position, destination);
}
else
{
EndianStream.CopyPartTo(source, destination, item.Position, item.Length, BufferSize);
}
}
};
if (EnableThreading)
{
Parallel.ForEach(items, new ParallelOptions { MaxDegreeOfParallelism = MaxThreads }, action);
}
else
{
items.ForEach(action);
}
items.Clear();
}
public DataExtractor()
{
BufferSize = 4096;
EnableThreading = true;
MaxThreads = 4;
}
}
}

View File

@ -1,54 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
namespace SonicAudioLib.IO
{
public class Helpers
{
public static long Align(long value, long alignment)
{
while ((value % alignment) != 0)
{
value++;
}
return value;
}
// This one masks the source to destination
public static void MaskCriTable(Stream source, Stream destination, long length)
{
uint currentXor = 25951;
long currentPosition = source.Position;
while (source.Position < currentPosition + length)
{
byte maskedByte = (byte)(EndianStream.ReadByte(source) ^ currentXor);
currentXor *= 16661;
EndianStream.WriteByte(destination, maskedByte);
}
}
// This one masks the source to itself
public static void MaskCriTable(Stream source, long length)
{
if (source.CanRead && source.CanWrite)
{
uint currentXor = 25951;
long currentPosition = source.Position;
while (source.Position < currentPosition + length)
{
byte maskedByte = (byte)(EndianStream.ReadByte(source) ^ currentXor);
currentXor *= 16661;
EndianStream.WriteByteAt(source, maskedByte, source.Position - 1);
}
}
}
}
}

View File

@ -12,6 +12,7 @@ namespace SonicAudioLib.IO
private long startPosition = 0;
private uint align = 1;
private long length = 0;
private long baseLength = 0;
public long Position
{
@ -37,6 +38,8 @@ namespace SonicAudioLib.IO
}
}
public event ProgressChanged ProgressChanged;
public long Put(byte[] data)
{
if (data == null || data.Length <= 0)
@ -132,6 +135,8 @@ namespace SonicAudioLib.IO
{
module.Write(destination);
}
ProgressChanged?.Invoke(this, new ProgressChangedEventArgs(((destination.Position - startPosition) / (double)(length - baseLength)) * 100.0));
}
}
@ -143,7 +148,9 @@ namespace SonicAudioLib.IO
public VldPool(uint align, long baseLength)
{
this.align = align;
length = baseLength;
this.baseLength = baseLength;
length = this.baseLength;
}
public VldPool(uint align)

View File

@ -8,6 +8,8 @@ namespace SonicAudioLib.Module
{
public abstract class ModuleBase
{
protected int bufferSize = 4096;
public abstract void Read(Stream source);
public abstract void Write(Stream destination);
@ -17,6 +19,8 @@ namespace SonicAudioLib.Module
{
Read(source);
}
this.bufferSize = bufferSize;
}
public virtual void Load(string sourceFileName)
@ -43,6 +47,8 @@ namespace SonicAudioLib.Module
{
Write(destination);
}
this.bufferSize = bufferSize;
}
public virtual byte[] Save()

View File

@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SonicAudioLib
{
public class ProgressChangedEventArgs : EventArgs
{
public double Progress { get; private set; }
public ProgressChangedEventArgs(double progress)
{
Progress = Math.Round(progress, 2, MidpointRounding.AwayFromZero);
}
}
public delegate void ProgressChanged(object sender, ProgressChangedEventArgs e);
}

View File

@ -20,6 +20,7 @@
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<AllowUnsafeBlocks>false</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
@ -58,11 +59,13 @@
<Compile Include="CriMw\Serialization\CriTableSerializer.cs" />
<Compile Include="CriMw\CriTableWriter.cs" />
<Compile Include="IO\EndianStream.cs" />
<Compile Include="IO\DataExtractor.cs" />
<Compile Include="IO\StringPool.cs" />
<Compile Include="IO\Substream.cs" />
<Compile Include="IO\Helpers.cs" />
<Compile Include="Helpers.cs" />
<Compile Include="IO\VldPool.cs" />
<Compile Include="Module\ModuleBase.cs" />
<Compile Include="ProgressChangedEvent.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup />