diff --git a/Experimental/Experimental.csproj b/Experimental/Experimental.csproj
index 82e29bbd..3285ec98 100644
--- a/Experimental/Experimental.csproj
+++ b/Experimental/Experimental.csproj
@@ -7,7 +7,7 @@
{60054FD9-4472-4BB4-9E3D-2F80D3D22468}
Library
Properties
- GameRes.Experimental
+ GameRes.Extra
ArcExtra
v4.5
512
@@ -81,6 +81,13 @@
+
+
+
+
+
+
+
@@ -97,6 +104,7 @@
+
PreserveNewest
diff --git a/Experimental/Unity/ArcUnityFS.cs b/Experimental/Unity/ArcUnityFS.cs
new file mode 100644
index 00000000..cb673286
--- /dev/null
+++ b/Experimental/Unity/ArcUnityFS.cs
@@ -0,0 +1,302 @@
+//! \file ArcUnityFS.cs
+//! \date Tue Apr 04 22:27:22 2017
+//! \brief Unity asset archive.
+//
+// Copyright (C) 2017 by morkt
+//
+// 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.
+//
+
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Linq;
+using System.Text;
+using GameRes.Compression;
+using GameRes.Utility;
+
+namespace GameRes.Formats.Unity
+{
+ [Export(typeof(ArchiveFormat))]
+ public class UnityFSOpener : ArchiveFormat
+ {
+ public override string Tag { get { return "UNITY/FS"; } }
+ public override string Description { get { return "Unity game engine asset archive"; } }
+ public override uint Signature { get { return 0x74696E55; } } // 'UnityFS'
+ public override bool IsHierarchic { get { return true; } }
+ public override bool CanWrite { get { return false; } }
+
+ public UnityFSOpener ()
+ {
+ Extensions = new string[] { "" };
+ }
+
+ public override ArcFile TryOpen (ArcView file)
+ {
+ if (!file.View.AsciiEqual (0, "UnityFS\0"))
+ return null;
+ int arc_version = Binary.BigEndian (file.View.ReadInt32 (8));
+ if (arc_version != 6)
+ return null;
+ long data_offset;
+ byte[] index_data;
+ using (var input = file.CreateStream())
+ {
+ input.Position = 0xC;
+ input.ReadCString (Encoding.UTF8);
+ input.ReadCString (Encoding.UTF8);
+ long file_size = Binary.BigEndian (input.ReadInt64());
+ int packed_index_size = Binary.BigEndian (input.ReadInt32());
+ int index_size = Binary.BigEndian (input.ReadInt32());
+ int flags = Binary.BigEndian (input.ReadInt32());
+ long index_offset;
+ if (0 == (flags & 0x80))
+ {
+ index_offset = input.Position;
+ data_offset = index_offset + packed_index_size;
+ }
+ else
+ {
+ index_offset = file_size - packed_index_size;
+ data_offset = input.Position;
+ }
+ input.Position = index_offset;
+ var packed = input.ReadBytes (packed_index_size);
+ switch (flags & 0x3F)
+ {
+ case 0:
+ index_data = packed;
+ break;
+ case 1:
+ index_data = UnpackLzma (packed, index_size);
+ break;
+ case 3:
+ index_data = new byte[index_size];
+ Lz4Compressor.DecompressBlock (packed, packed.Length, index_data, index_data.Length);
+ break;
+ default:
+ return null;
+ }
+ }
+ var index = new AssetDeserializer (file, data_offset);
+ using (var input = new BinMemoryStream (index_data))
+ index.Parse (input);
+ var dir = index.LoadObjects();
+ return new UnityBundle (file, this, dir, index.Segments);
+ }
+
+ public override Stream OpenEntry (ArcFile arc, Entry entry)
+ {
+ var uarc = (UnityBundle)arc;
+ var input = new BundleStream (uarc.File, uarc.Segments);
+ return new StreamRegion (input, entry.Offset, entry.Size);
+ }
+
+ internal static byte[] UnpackLzma (byte[] input, int unpacked_size)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ internal class BundleEntry : Entry
+ {
+ public uint Flags;
+ }
+
+ internal class AssetEntry : Entry
+ {
+ public BundleEntry Bundle;
+ public UnityObject AssetObject;
+ }
+
+ internal class UnityBundle : ArcFile
+ {
+ public readonly List Segments;
+
+ public UnityBundle (ArcView arc, ArchiveFormat impl, ICollection dir, List segments)
+ : base (arc, impl, dir)
+ {
+ Segments = segments;
+ }
+ }
+
+ internal class BundleSegment
+ {
+ public long Offset;
+ public uint PackedSize;
+ public long UnpackedOffset;
+ public uint UnpackedSize;
+ public int Compression;
+
+ public bool IsCompressed { get { return Compression != 0; } }
+ }
+
+ internal class AssetDeserializer
+ {
+ readonly ArcView m_file;
+ readonly long m_data_offset;
+ List m_segments;
+ List m_bundles;
+
+ public List Segments { get { return m_segments; } }
+
+ public AssetDeserializer (ArcView file, long data_offset)
+ {
+ m_file = file;
+ m_data_offset = data_offset;
+ }
+
+ public void Parse (IBinaryStream index)
+ {
+ index.Position = 16;
+ int segment_count = Binary.BigEndian (index.ReadInt32());
+ m_segments = new List (segment_count);
+ long packed_offset = m_data_offset;
+ long unpacked_offset = 0;
+ for (int i = 0; i < segment_count; ++i)
+ {
+ var segment = new BundleSegment();
+ segment.Offset = packed_offset;
+ segment.UnpackedOffset = unpacked_offset;
+ segment.UnpackedSize = Binary.BigEndian (index.ReadUInt32());
+ segment.PackedSize = Binary.BigEndian (index.ReadUInt32());
+ segment.Compression = Binary.BigEndian (index.ReadUInt16());
+ m_segments.Add (segment);
+ packed_offset += segment.PackedSize;
+ unpacked_offset += segment.UnpackedSize;
+ }
+ int count = Binary.BigEndian (index.ReadInt32());
+ m_bundles = new List (count);
+ for (int i = 0; i < count; ++i)
+ {
+ var entry = new BundleEntry();
+ entry.Offset = Binary.BigEndian (index.ReadInt64());
+ entry.Size = (uint)Binary.BigEndian (index.ReadInt64());
+ entry.Flags = Binary.BigEndian (index.ReadUInt32());
+ entry.Name = index.ReadCString (Encoding.UTF8);
+ m_bundles.Add (entry);
+ }
+ }
+
+ public List LoadObjects ()
+ {
+ var dir = new List();
+ using (var stream = new BundleStream (m_file, m_segments))
+ {
+ foreach (BundleEntry bundle in m_bundles)
+ {
+ if (bundle.Name.EndsWith (".resource"))
+ continue;
+ using (var asset_stream = new StreamRegion (stream, bundle.Offset, bundle.Size, true))
+ using (var reader = new AssetReader (asset_stream, bundle.Name))
+ {
+ var asset = new Asset();
+ asset.Load (reader);
+ var object_dir = ParseAsset (bundle, asset, stream);
+ dir.AddRange (object_dir);
+ }
+ }
+ if (0 == dir.Count)
+ dir.AddRange (m_bundles);
+ }
+ return dir;
+ }
+
+ IEnumerable ParseAsset (BundleEntry bundle, Asset asset, Stream file)
+ {
+ Dictionary id_map = null;
+ var bundle_types = asset.Tree.TypeTrees.Where (t => t.Value.Type == "AssetBundle").Select (t => t.Key);
+ if (bundle_types.Any())
+ {
+ // try to read entry names from AssetBundle object
+ int bundle_type_id = bundle_types.First();
+ var asset_bundle = asset.Objects.FirstOrDefault (x => x.TypeId == bundle_type_id);
+ if (asset_bundle != null)
+ {
+ id_map = asset.ReadAssetBundle (file, asset_bundle);
+ }
+ }
+ if (null == id_map)
+ id_map = new Dictionary();
+ foreach (var obj in asset.Objects)
+ {
+ string type = obj.Type;
+ AssetEntry entry = null;
+ if ("AudioClip" == type)
+ {
+ entry = ReadAudioClip (file, obj, asset);
+ }
+ if (null == entry)
+ {
+ entry = new AssetEntry {
+ Type = type,
+ Bundle = bundle,
+ AssetObject = obj,
+ Offset = obj.Offset,
+ Size = obj.Size,
+ };
+ }
+ string name;
+ if (!id_map.TryGetValue (obj.PathId, out name))
+ name = obj.PathId.ToString ("X16");
+ else
+ name = ShortenPath (name);
+ entry.Name = name;
+ yield return entry;
+ }
+ }
+
+ AssetEntry ReadAudioClip (Stream input, UnityObject obj, Asset asset)
+ {
+ using (var stream = new StreamRegion (input, obj.Offset, obj.Size, true))
+ using (var reader = new AssetReader (stream, ""))
+ {
+ reader.SetupReaders (asset.Format, asset.IsLittleEndian);
+ var clip = new AudioClip();
+ clip.Load (reader);
+ var bundle_name = Path.GetFileName (clip.m_Source);
+ var bundle = m_bundles.FirstOrDefault (b => b.Name == bundle_name);
+ if (null == bundle)
+ return null;
+ return new AssetEntry {
+ Type = "audio",
+ Bundle = bundle,
+ AssetObject = obj,
+ Offset = bundle.Offset + clip.m_Offset,
+ Size = (uint)clip.m_Size,
+ };
+ }
+ }
+
+ ///
+ /// Shorten asset path to contain only the bottom directory component.
+ ///
+ static string ShortenPath (string name)
+ {
+ int slash_pos = name.LastIndexOf ('/');
+ if (-1 == slash_pos)
+ return name;
+ slash_pos = name.LastIndexOf ('/', slash_pos-1);
+ if (-1 == slash_pos)
+ return name;
+ return name.Substring (slash_pos+1);
+ }
+ }
+}
diff --git a/Experimental/Unity/Asset.cs b/Experimental/Unity/Asset.cs
new file mode 100644
index 00000000..3e03f1f7
--- /dev/null
+++ b/Experimental/Unity/Asset.cs
@@ -0,0 +1,454 @@
+//! \file Asset.cs
+//! \date Wed Apr 05 18:58:07 2017
+//! \brief Unity asset class.
+//
+// Based on the [UnityPack](https://github.com/HearthSim/UnityPack)
+//
+// Copyright (c) Jerome Leclanche
+//
+// C# port copyright (C) 2017 by morkt
+//
+// 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.
+//
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using GameRes.Utility;
+
+namespace GameRes.Formats.Unity
+{
+ internal class Asset
+ {
+ int m_header_size;
+ int m_format;
+ uint m_data_offset;
+ bool m_is_little_endian;
+ UnityTypeData m_tree = new UnityTypeData();
+ Dictionary m_adds;
+ List m_refs;
+ Dictionary m_types = new Dictionary();
+ Dictionary m_objects = new Dictionary();
+
+ public int Format { get { return m_format; } }
+ public bool IsLittleEndian { get { return m_is_little_endian; } }
+ public long DataOffset { get { return m_data_offset; } }
+ public UnityTypeData Tree { get { return m_tree; } }
+ public IEnumerable Objects { get { return m_objects.Values; } }
+
+ public void Load (AssetReader input)
+ {
+ m_header_size = input.ReadInt32();
+ input.ReadUInt32(); // file_size
+ m_format = input.ReadInt32();
+ m_data_offset = input.ReadUInt32();
+ if (m_format >= 9)
+ m_is_little_endian = 0 == input.ReadInt32();
+ input.SetupReaders (m_format, m_is_little_endian);
+ m_tree.Load (input);
+
+ bool long_ids = Format >= 14;
+ if (Format >= 7 && Format < 14)
+ long_ids = 0 != input.ReadInt32();
+ input.SetupReadId (long_ids);
+
+ int obj_count = input.ReadInt32();
+ for (int i = 0; i < obj_count; ++i)
+ {
+ input.Align();
+ var obj = new UnityObject (this);
+ obj.Load (input);
+ RegisterObject (obj);
+ }
+ if (Format >= 11)
+ {
+ int count = input.ReadInt32();
+ m_adds = new Dictionary (count);
+ for (int i = 0; i < count; ++i)
+ {
+ input.Align();
+ var id = input.ReadId();
+ m_adds[id] = input.ReadInt32();
+ }
+ }
+ if (Format >= 6)
+ {
+ int count = input.ReadInt32();
+ m_refs = new List (count);
+ for (int i = 0; i < count; ++i)
+ {
+ var asset_ref = AssetRef.Load (input);
+ m_refs.Add (asset_ref);
+ }
+ }
+ input.ReadCString();
+ }
+
+ void RegisterObject (UnityObject obj)
+ {
+ if (m_tree.TypeTrees.ContainsKey (obj.TypeId))
+ {
+ m_types[obj.TypeId] = m_tree.TypeTrees[obj.TypeId];
+ }
+ else if (!m_types.ContainsKey (obj.TypeId))
+ {
+ /*
+ var trees = TypeTree.Default (this).TypeTrees;
+ if (trees.ContainsKey (obj.ClassId))
+ {
+ m_types[obj.TypeId] = trees[obj.ClassId];
+ }
+ else
+ */
+ {
+ Trace.WriteLine ("Unknown type id", obj.ClassId.ToString());
+ m_types[obj.TypeId] = null;
+ }
+ throw new ApplicationException (string.Format ("Unknwon type id {0}", obj.ClassId));
+ }
+ if (m_objects.ContainsKey (obj.PathId))
+ throw new ApplicationException (string.Format ("Duplicate asset object {0} (PathId: {1})", obj, obj.PathId));
+ m_objects[obj.PathId] = obj;
+ }
+
+ public Dictionary ReadAssetBundle (Stream input, UnityObject bundle)
+ {
+ using (var stream = new StreamRegion (input, bundle.Offset, bundle.Size, true))
+ using (var reader = new AssetReader (stream, ""))
+ {
+ reader.SetupReaders (m_format, m_is_little_endian);
+ var name = reader.ReadString(); // m_Name
+ reader.Align();
+ var id_map = new Dictionary();
+ id_map[bundle.PathId] = name;
+ int count = reader.ReadInt32(); // m_PreloadTable
+ for (int i = 0; i < count; ++i)
+ {
+ reader.ReadInt32(); // m_FileID
+ reader.ReadInt64(); // m_PathID
+ }
+ count = reader.ReadInt32(); // m_Container
+ for (int i = 0; i < count; ++i)
+ {
+ name = reader.ReadString();
+ reader.Align();
+ reader.ReadInt32(); // preloadIndex
+ reader.ReadInt32(); // preloadSize
+ reader.ReadInt32(); // m_FileID
+ long id = reader.ReadInt64();
+ id_map[id] = name;
+ }
+ return id_map;
+ }
+ }
+ }
+
+ internal class AssetRef
+ {
+ public string AssetPath;
+ public Guid Guid;
+ public int Type;
+ public string FilePath;
+ public object Asset;
+
+ public static AssetRef Load (AssetReader reader)
+ {
+ var r = new AssetRef();
+ r.AssetPath = reader.ReadCString();
+ r.Guid = new Guid (reader.ReadBytes (16));
+ r.Type = reader.ReadInt32();
+ r.FilePath = reader.ReadCString();
+ r.Asset = null;
+ return r;
+ }
+ }
+
+ internal class UnityObject
+ {
+ public Asset Asset;
+ public long PathId;
+ public long Offset;
+ public uint Size;
+ public int TypeId;
+ public int ClassId;
+ public bool IsDestroyed;
+
+ public UnityObject (Asset owner)
+ {
+ Asset = owner;
+ }
+
+ public void Load (AssetReader reader)
+ {
+ PathId = reader.ReadId();
+ Offset = reader.ReadUInt32() + Asset.DataOffset;
+ Size = reader.ReadUInt32();
+ if (Asset.Format < 17)
+ {
+ TypeId = reader.ReadInt32();
+ ClassId = reader.ReadInt16();
+ }
+ else
+ {
+ var type_id = reader.ReadInt32();
+ var class_id = Asset.Tree.ClassIds[type_id];
+ TypeId = class_id;
+ ClassId = class_id;
+ }
+ if (Asset.Format <= 10)
+ IsDestroyed = reader.ReadInt16() != 0;
+ if (Asset.Format >= 11 && Asset.Format < 17)
+ reader.ReadInt16();
+ if (Asset.Format >= 15 && Asset.Format < 17)
+ reader.ReadByte();
+ }
+
+ public string Type {
+ get {
+ var type_tree = Asset.Tree.TypeTrees;
+ if (type_tree.ContainsKey (TypeId))
+ return type_tree[TypeId].Type;
+ return string.Format ("[TypeId:{0}]", TypeId);
+ }
+ }
+
+ public override string ToString ()
+ {
+ return string.Format ("<{0} {1}>", Type, ClassId);
+ }
+ }
+
+ internal class TypeTree
+ {
+ int m_format;
+ List m_children = new List();
+
+ public int Version;
+ public bool IsArray;
+ public string Type;
+ public string Name;
+ public int Size;
+ public uint Index;
+ public int Flags;
+
+ public IList Children { get { return m_children; } }
+
+ static readonly string Null = "(null)";
+ static readonly Lazy StringsDat = new Lazy (() => LoadResource ("strings.dat"));
+
+ public TypeTree (int format)
+ {
+ m_format = format;
+ }
+
+ public void Load (AssetReader reader)
+ {
+ if (10 == m_format || m_format >= 12)
+ LoadBlob (reader);
+ else
+ LoadRaw (reader);
+ }
+
+ void LoadRaw (AssetReader reader)
+ {
+ throw new NotImplementedException();
+ }
+
+ byte[] m_data;
+
+ void LoadBlob (AssetReader reader)
+ {
+ int count = reader.ReadInt32();
+ int buffer_bytes = reader.ReadInt32();
+ var node_data = reader.ReadBytes (24 * count);
+ m_data = reader.ReadBytes (buffer_bytes);
+
+ var parents = new Stack();
+ parents.Push (this);
+ using (var buf = new BinMemoryStream (node_data))
+ {
+ for (int i = 0; i < count; ++i)
+ {
+ int version = buf.ReadInt16();
+ int depth = buf.ReadUInt8();
+ TypeTree current;
+ if (0 == depth)
+ {
+ current = this;
+ }
+ else
+ {
+ while (parents.Count > depth)
+ parents.Pop();
+ current = new TypeTree (m_format);
+ parents.Peek().Children.Add (current);
+ parents.Push (current);
+ }
+ current.Version = version;
+ current.IsArray = buf.ReadUInt8() != 0;
+ current.Type = GetString (buf.ReadInt32());
+ current.Name = GetString (buf.ReadInt32());
+ current.Size = buf.ReadInt32();
+ current.Index = buf.ReadUInt32();
+ current.Flags = buf.ReadInt32();
+ }
+ }
+ }
+
+ string GetString (int offset)
+ {
+ byte[] strings;
+ if (offset < 0)
+ {
+ offset &= 0x7FFFFFFF;
+ strings = StringsDat.Value;
+ }
+ else if (offset < m_data.Length)
+ strings = m_data;
+ else
+ return Null;
+ return Binary.GetCString (strings, offset, strings.Length-offset, Encoding.UTF8);
+ }
+
+ internal static Stream OpenResource (string name)
+ {
+ var qualified_name = ".Unity." + name;
+ var assembly = Assembly.GetExecutingAssembly();
+ var res_name = assembly.GetManifestResourceNames().Single (r => r.EndsWith (qualified_name));
+ Stream stream = assembly.GetManifestResourceStream (res_name);
+ if (null == stream)
+ throw new FileNotFoundException ("Resource not found.", name);
+ return stream;
+ }
+
+ internal static byte[] LoadResource (string name)
+ {
+ using (var stream = OpenResource (name))
+ {
+ var res = new byte[stream.Length];
+ stream.Read (res, 0, res.Length);
+ return res;
+ }
+ }
+ }
+
+ internal class UnityTypeData
+ {
+ List m_class_ids = new List ();
+ Dictionary m_hashes = new Dictionary ();
+ Dictionary m_type_trees = new Dictionary ();
+
+ public IList ClassIds { get { return m_class_ids; } }
+ public IDictionary Hashes { get { return m_hashes; } }
+ public IDictionary TypeTrees { get { return m_type_trees; } }
+
+ public void Load (AssetReader reader)
+ {
+ int format = reader.Format;
+ var version = reader.ReadCString();
+ var platform = reader.ReadInt32 ();
+ if (format >= 13)
+ {
+ bool has_type_trees = reader.ReadBool ();
+ int count = reader.ReadInt32 ();
+ for (int i = 0; i < count; ++i)
+ {
+ int class_id = reader.ReadInt32 ();
+ if (format >= 17)
+ {
+ reader.ReadByte ();
+ int script_id = reader.ReadInt16 ();
+ if (114 == class_id)
+ {
+ if (script_id >= 0)
+ class_id = -2 - script_id;
+ else
+ class_id = -1;
+ }
+ }
+ m_class_ids.Add (class_id);
+ byte[] hash = reader.ReadBytes (class_id < 0 ? 0x20 : 0x10);
+ m_hashes[class_id] = hash;
+ if (has_type_trees)
+ {
+ var tree = new TypeTree (format);
+ tree.Load (reader);
+ m_type_trees[class_id] = tree;
+ }
+ }
+ }
+ else
+ {
+ int count = reader.ReadInt32 ();
+ for (int i = 0; i < count; ++i)
+ {
+ int class_id = reader.ReadInt32 ();
+ var tree = new TypeTree (format);
+ tree.Load (reader);
+ m_type_trees[class_id] = tree;
+ }
+ }
+ }
+ }
+
+ internal class AudioClip
+ {
+ public int m_LoadType;
+ public int m_Channels;
+ public int m_Frequency;
+ public int m_BitsPerSample;
+ public float m_Length;
+ public bool m_IsTrackerFormat;
+ public int m_SubsoundIndex;
+ public bool m_PreloadAudioData;
+ public bool m_LoadInBackground;
+ public bool m_Legacy3D;
+ public string m_Source;
+ public long m_Offset;
+ public long m_Size;
+ public int m_CompressionFormat;
+
+ public void Load (AssetReader reader)
+ {
+ var name = reader.ReadString();
+ reader.Align();
+ m_LoadType = reader.ReadInt32();
+ m_Channels = reader.ReadInt32();
+ m_Frequency = reader.ReadInt32();
+ m_BitsPerSample = reader.ReadInt32();
+ m_Length = reader.ReadFloat();
+ m_IsTrackerFormat = reader.ReadBool();
+ reader.Align();
+ m_SubsoundIndex = reader.ReadInt32();
+ m_PreloadAudioData = reader.ReadBool();
+ m_LoadInBackground = reader.ReadBool();
+ m_Legacy3D = reader.ReadBool();
+ reader.Align();
+ m_Source = reader.ReadString();
+ reader.Align();
+ m_Offset = reader.ReadInt64();
+ m_Size = reader.ReadInt64();
+ m_CompressionFormat = reader.ReadInt32();
+ }
+ }
+}
diff --git a/Experimental/Unity/AssetReader.cs b/Experimental/Unity/AssetReader.cs
new file mode 100644
index 00000000..09a75624
--- /dev/null
+++ b/Experimental/Unity/AssetReader.cs
@@ -0,0 +1,194 @@
+//! \file AssetReader.cs
+//! \date Wed Apr 05 13:28:33 2017
+//! \brief Unity asset reader class.
+//
+// Copyright (C) 2017 by morkt
+//
+// 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.
+//
+
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Text;
+using GameRes.Utility;
+
+namespace GameRes.Formats.Unity
+{
+ ///
+ /// AssetReader provides access to a serialized stream of Unity assets.
+ ///
+ internal sealed class AssetReader : IDisposable
+ {
+ IBinaryStream m_input;
+ int m_format;
+
+ public int Format { get { return m_format; } }
+ public long Position {
+ get { return m_input.Position; }
+ set { m_input.Position = value; }
+ }
+
+ public AssetReader (Stream input, string name)
+ {
+ m_input = BinaryStream.FromStream (input, name);
+ SetupReaders (0, false);
+ }
+
+ public Action Align;
+ public Func ReadUInt16;
+ public Func ReadInt16;
+ public Func ReadUInt32;
+ public Func ReadInt32;
+ public Func ReadInt64;
+ public Func ReadId;
+
+ ///
+ /// Setup reader endianness accordingly.
+ ///
+ public void SetupReaders (int format, bool is_little_endian)
+ {
+ m_format = format;
+ if (is_little_endian)
+ {
+ ReadUInt16 = () => m_input.ReadUInt16();
+ ReadUInt32 = () => m_input.ReadUInt32();
+ ReadInt16 = () => m_input.ReadInt16();
+ ReadInt32 = () => m_input.ReadInt32();
+ ReadInt64 = () => m_input.ReadInt64();
+ }
+ else
+ {
+ ReadUInt16 = () => Binary.BigEndian (m_input.ReadUInt16());
+ ReadUInt32 = () => Binary.BigEndian (m_input.ReadUInt32());
+ ReadInt16 = () => Binary.BigEndian (m_input.ReadInt16());
+ ReadInt32 = () => Binary.BigEndian (m_input.ReadInt32());
+ ReadInt64 = () => Binary.BigEndian (m_input.ReadInt64());
+ }
+ if (m_format >= 14)
+ {
+ Align = () => {
+ long pos = m_input.Position;
+ if (0 != (pos & 3))
+ m_input.Position = (pos + 3) & ~3L;
+ };
+ ReadId = ReadInt64;
+ }
+ else
+ {
+ Align = () => {};
+ ReadId = () => ReadInt32();
+ }
+ }
+
+ ///
+ /// Set asset ID length. If is true IDs are 64-bit, otherwise 32-bit.
+ ///
+ public void SetupReadId (bool long_ids)
+ {
+ if (long_ids)
+ ReadId = ReadInt64;
+ else
+ ReadId = () => ReadInt32();
+ }
+
+ ///
+ /// Read bytes into specified buffer.
+ ///
+ public int Read (byte[] buffer, int offset, int count)
+ {
+ return m_input.Read (buffer, offset, count);
+ }
+
+ ///
+ /// Read null-terminated UTF8 string.
+ ///
+ public string ReadCString ()
+ {
+ return m_input.ReadCString (Encoding.UTF8);
+ }
+
+ ///
+ /// Read UTF8 string prefixed with length.
+ ///
+ public string ReadString ()
+ {
+ int length = ReadInt32();
+ if (0 == length)
+ return string.Empty;
+ var bytes = ReadBytes (length);
+ return Encoding.UTF8.GetString (bytes);
+ }
+
+ ///
+ /// Read bytes from stream and return them in a byte array.
+ /// May return less than bytes if end of file was encountered.
+ ///
+ public byte[] ReadBytes (int length)
+ {
+ return m_input.ReadBytes (length);
+ }
+
+ ///
+ /// Read unsigned 8-bits byte from a stream.
+ ///
+ public byte ReadByte ()
+ {
+ return m_input.ReadUInt8();
+ }
+
+ ///
+ /// Read byte and interpret is as a bool value, non-zero resulting in true.
+ ///
+ public bool ReadBool ()
+ {
+ return ReadByte() != 0;
+ }
+
+ [StructLayout(LayoutKind.Explicit)]
+ struct Union
+ {
+ [FieldOffset (0)]
+ public uint u;
+ [FieldOffset(0)]
+ public float f;
+ }
+
+ ///
+ /// Read float value from a stream.
+ ///
+ public float ReadFloat ()
+ {
+ var buf = new Union();
+ buf.u = ReadUInt32();
+ return buf.f;
+ }
+
+ bool _disposed = false;
+ public void Dispose ()
+ {
+ if (!_disposed)
+ {
+ m_input.Dispose();
+ _disposed = true;
+ }
+ GC.SuppressFinalize (this);
+ }
+ }
+}
diff --git a/Experimental/Unity/AudioFSB5.cs b/Experimental/Unity/AudioFSB5.cs
new file mode 100644
index 00000000..6561e3f9
--- /dev/null
+++ b/Experimental/Unity/AudioFSB5.cs
@@ -0,0 +1,402 @@
+//! \file AudioFSB5.cs
+//! \date Thu Apr 06 01:12:27 2017
+//! \brief FMOD Sample Bank audio file.
+//
+// Based on [python-fsb5](https://github.com/HearthSim/python-fsb5)
+//
+// Copyright (c) 2016 Simon Pinfold
+//
+// C# implementation Copyright (C) 2017 by morkt
+//
+// 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.
+//
+
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.IO;
+using GameRes.Formats.Vorbis;
+
+namespace GameRes.Formats.Fmod
+{
+ [Export(typeof(AudioFormat))]
+ public class Fsb5Audio : AudioFormat
+ {
+ public override string Tag { get { return "FSB5"; } }
+ public override string Description { get { return "FMOD Sample Bank audio format"; } }
+ public override uint Signature { get { return 0x35425346; } } // 'FSB5'
+ public override bool CanWrite { get { return false; } }
+
+ public override SoundInput TryOpen (IBinaryStream file)
+ {
+ var fsb = new Fsb5Decoder (file);
+ var sound = fsb.Convert();
+ file.Dispose();
+ return sound;
+ }
+
+ public override ResourceScheme Scheme
+ {
+ get { return new FmodScheme { VorbisHeaders = Fsb5Decoder.VorbisHeaders }; }
+ set { Fsb5Decoder.VorbisHeaders = ((FmodScheme)value).VorbisHeaders; }
+ }
+ }
+
+ enum SoundFormat
+ {
+ // Order is crucial.
+ None,
+ Pcm8,
+ Pcm16,
+ Pcm24,
+ Pcm32,
+ PcmFloat,
+ GcAdpcm,
+ ImaAdpcm,
+ Vag,
+ Hevag,
+ Xma,
+ Mpeg,
+ Celt,
+ At9,
+ Xwma,
+ Vorbis,
+ }
+
+ enum ChunkType
+ {
+ Channels = 1,
+ SampleRate = 2,
+ Loop = 3,
+ VorbisData = 11,
+ }
+
+ internal class Sample
+ {
+ public int SampleRate;
+ public ushort Channels;
+ public long DataOffset;
+ public int SampleCount;
+ public byte[] Data;
+ public Dictionary MetaData;
+ }
+
+ internal class Fsb5Decoder
+ {
+ IBinaryStream m_input;
+ int m_sample_header_size;
+ int m_name_table_size;
+ int m_header_size;
+ int m_data_size;
+ SoundFormat m_format;
+
+ static readonly HashSet Supported = new HashSet {
+ SoundFormat.Pcm8,
+ SoundFormat.Pcm16,
+ SoundFormat.Pcm32,
+ SoundFormat.PcmFloat,
+ SoundFormat.Vorbis,
+ };
+
+ public Fsb5Decoder (IBinaryStream input)
+ {
+ m_input = input;
+ }
+
+ List ReadSamples ()
+ {
+ var header = m_input.ReadHeader (0x3C);
+ int version = header.ToInt32 (4);
+ int sample_count = header.ToInt32 (8);
+ m_sample_header_size = header.ToInt32 (0xC);
+ m_name_table_size = header.ToInt32 (0x10);
+ m_data_size = header.ToInt32 (0x14);
+ m_format = (SoundFormat)header.ToInt32 (0x18);
+ if (!Supported.Contains (m_format))
+ throw new NotSupportedException();
+
+ if (0 == version)
+ m_input.ReadInt32();
+ var samples = new List (sample_count);
+ m_header_size = (int)m_input.Position;
+ for (int i = 0; i < sample_count; ++i)
+ {
+ long raw = m_input.ReadInt64();
+ bool next_chunk = 0 != (raw & 1);
+ int sample_rate = (int)((raw >> 1) & 0xF);
+ ushort channels = (ushort)(((raw >> 5) & 1) + 1);
+ long data_offset = ((raw >> 6) & 0xFFFFFFF) * 0x10;
+ int count = (int)((raw >> 34) & 0x3FFFFFFF);
+ var chunks = new Dictionary();
+ while (next_chunk)
+ {
+ int d = m_input.ReadInt32();
+ next_chunk = 0 != (d & 1);
+ int chunk_size = (d >> 1) & 0xFFFFFF;
+ var chunk_type = (ChunkType)((d >> 25) & 0x7F);
+ object chunk;
+ switch (chunk_type)
+ {
+ case ChunkType.Channels:
+ chunk = m_input.ReadUInt8();
+ break;
+ case ChunkType.SampleRate:
+ chunk = m_input.ReadInt32();
+ break;
+ case ChunkType.Loop:
+ int v1 = m_input.ReadInt32();
+ int v2 = m_input.ReadInt32();
+ chunk = Tuple.Create (v1, v2);
+ break;
+ case ChunkType.VorbisData:
+ chunk = new VorbisData {
+ Crc32 = m_input.ReadUInt32(),
+ Data = m_input.ReadBytes (chunk_size-4) // XXX unused
+ };
+ break;
+ default:
+ chunk = m_input.ReadBytes (chunk_size);
+ break;
+ }
+ chunks[chunk_type] = chunk;
+ }
+ if (chunks.ContainsKey (ChunkType.SampleRate))
+ sample_rate = (int)chunks[ChunkType.SampleRate];
+ else if (SampleRates.ContainsKey (sample_rate))
+ sample_rate = SampleRates[sample_rate];
+ else
+ throw new InvalidFormatException ("Invalid FSB5 sample rate.");
+
+ var sample = new Sample {
+ SampleRate = sample_rate,
+ Channels = channels,
+ DataOffset = data_offset,
+ SampleCount = count,
+ MetaData = chunks,
+ Data = null
+ };
+ samples.Add (sample);
+ }
+ return samples;
+ }
+
+ public SoundInput Convert ()
+ {
+ var samples = ReadSamples();
+ var sample = samples[0];
+ int data_length;
+ if (samples.Count > 1)
+ data_length = (int)(samples[1].DataOffset - sample.DataOffset);
+ else
+ data_length = m_data_size;
+ m_input.Position = m_header_size + m_sample_header_size + m_name_table_size;
+ sample.Data = m_input.ReadBytes (data_length);
+
+ if (SoundFormat.Vorbis == m_format)
+ return RebuildVorbis (sample);
+ else
+ return RebuildPcm (sample);
+ }
+
+ SoundInput RebuildPcm (Sample sample)
+ {
+ var format = new WaveFormat
+ {
+ FormatTag = (ushort)(SoundFormat.PcmFloat == m_format ? 3 : 1),
+ Channels = sample.Channels,
+ SamplesPerSecond = (uint)sample.SampleRate,
+ };
+ switch (m_format)
+ {
+ case SoundFormat.Pcm8: format.BitsPerSample = 8; break;
+ case SoundFormat.Pcm16: format.BitsPerSample = 16; break;
+ case SoundFormat.PcmFloat:
+ case SoundFormat.Pcm32: format.BitsPerSample = 32; break;
+ default: throw new InvalidFormatException();
+ }
+ format.BlockAlign = (ushort)(format.Channels * format.BitsPerSample / 8);
+ format.SetBPS();
+ var pcm = new MemoryStream (sample.Data);
+ return new RawPcmInput (pcm, format);
+ }
+
+ SoundInput RebuildVorbis (Sample sample)
+ {
+ if (!sample.MetaData.ContainsKey (ChunkType.VorbisData))
+ throw new InvalidFormatException ("No VORBISDATA chunk in FSB5 Vorbis stream.");
+ var vorbis_data = sample.MetaData[ChunkType.VorbisData] as VorbisData;
+ var setup_data = GetVorbisHeader (vorbis_data.Crc32);
+ var state = new OggStreamState (1);
+
+ var id_packet = RebuildIdPacket (sample, 0x100, 0x800);
+ var comment_packet = RebuildCommentPacket();
+ var setup_packet = RebuildSetupPacket (setup_data);
+ var info = CreateVorbisInfo (sample, setup_packet);
+
+ var output = new MemoryStream();
+ state.PacketIn (id_packet);
+ state.Write (output);
+ state.PacketIn (comment_packet);
+ state.Write (output);
+ state.PacketIn (setup_packet);
+ state.Write (output);
+ state.Flush (output);
+
+ long packet_no = setup_packet.PacketNo + 1;
+ long granule_pos = 0;
+ int prev_block_size = 0;
+ using (var input = new BinMemoryStream (sample.Data))
+ {
+ var packet = new OggPacket();
+ int packet_size = ReadPacketSize (input);
+ while (packet_size > 0)
+ {
+ packet.SetPacket (packet_no++, input.ReadBytes (packet_size));
+ packet_size = ReadPacketSize (input);
+ packet.EoS = 0 == packet_size;
+
+ int block_size = info.PacketBlockSize (packet);
+ if (prev_block_size != 0)
+ granule_pos += (block_size + prev_block_size) / 4;
+ else
+ granule_pos = 0;
+ packet.GranulePos = granule_pos;
+ prev_block_size = block_size;
+
+ state.PacketIn (packet);
+ state.Write (output);
+ }
+ }
+ output.Position = 0;
+ return new OggInput (output);
+ }
+
+ VorbisInfo CreateVorbisInfo (Sample sample, OggPacket setup_packet)
+ {
+ var info = new VorbisInfo {
+ Channels = sample.Channels,
+ Rate = sample.SampleRate,
+ };
+ info.CodecSetup.BlockSizes[0] = 0x100;
+ info.CodecSetup.BlockSizes[1] = 0x800;
+ var comment = new VorbisComment { Vendor = VorbisComment.EncodeVendorString };
+ info.SynthesisHeaderin (comment, setup_packet);
+ return info;
+ }
+
+ int ReadPacketSize (IBinaryStream input)
+ {
+ int lo = input.ReadByte();
+ if (-1 == lo)
+ return 0;
+ int hi = input.ReadByte();
+ if (-1 == hi)
+ return 0;
+ return hi << 8 | lo;
+ }
+
+ OggPacket RebuildIdPacket (Sample sample, uint blocksize_short, uint blocksize_long)
+ {
+ using (var buf = new MemoryStream())
+ using (var output = new BinaryWriter (buf))
+ {
+ output.Write ((byte)1);
+ output.Write ("vorbis".ToCharArray());
+ output.Write (0);
+ output.Write ((byte)sample.Channels);
+ output.Write (sample.SampleRate);
+ output.Write (0);
+ output.Write (0);
+ output.Write (0);
+ int lo = VorbisInfo.CountBits (blocksize_short);
+ int hi = VorbisInfo.CountBits (blocksize_long);
+ int bits = hi << 4 | lo;
+ output.Write ((byte)bits);
+ output.Write ((byte)1);
+ output.Flush();
+
+ var packet = new OggPacket();
+ packet.SetPacket (0, buf.ToArray());
+ packet.BoS = true;
+ return packet;
+ }
+ }
+
+ OggPacket RebuildCommentPacket ()
+ {
+ var comment = new VorbisComment();
+ var packet = new OggPacket();
+ comment.HeaderOut (packet);
+ return packet;
+ }
+
+ OggPacket RebuildSetupPacket (byte[] setup_packet)
+ {
+ var packet = new OggPacket();
+ packet.SetPacket (2, setup_packet);
+ return packet;
+ }
+
+ static readonly Dictionary SampleRates = new Dictionary {
+ { 1, 8000 },
+ { 2, 11000 },
+ { 3, 11025 },
+ { 4, 16000 },
+ { 5, 22050 },
+ { 6, 24000 },
+ { 7, 32000 },
+ { 8, 44100 },
+ { 9, 48000 },
+ };
+
+ public static byte[] GetVorbisHeader (uint id)
+ {
+ FmodVorbisSetup setup;
+ if (!VorbisHeaders.TryGetValue (id, out setup))
+ throw new InvalidFormatException (string.Format ("Unknown FSB5 Vorbis encoding 0x{0:X8}.", id));
+ if (null == setup.PatchData || 0 == setup.PatchData.Length)
+ return setup.VorbisData;
+ var data = setup.VorbisData.Clone() as byte[];
+ Buffer.BlockCopy (setup.PatchData, 0, data, setup.PatchOffset, setup.PatchData.Length);
+ return data;
+ }
+
+ internal static Dictionary VorbisHeaders = new Dictionary();
+ }
+
+ internal class VorbisData
+ {
+ public uint Crc32;
+ public byte[] Data; // ignored
+ }
+
+ [Serializable]
+ public class FmodVorbisSetup
+ {
+ public byte[] VorbisData;
+ public int PatchOffset;
+ public byte[] PatchData;
+ }
+
+ [Serializable]
+ public class FmodScheme : ResourceScheme
+ {
+ public Dictionary VorbisHeaders;
+ }
+}
diff --git a/Experimental/Unity/BundleStream.cs b/Experimental/Unity/BundleStream.cs
new file mode 100644
index 00000000..8afaf826
--- /dev/null
+++ b/Experimental/Unity/BundleStream.cs
@@ -0,0 +1,201 @@
+//! \file BundleStream.cs
+//! \date Wed Apr 05 13:30:19 2017
+//! \brief Stream representing Unity bundle.
+//
+// Copyright (C) 2017 by morkt
+//
+// 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.
+//
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using GameRes.Compression;
+
+namespace GameRes.Formats.Unity
+{
+ ///
+ /// Stream representing Unity asset bundle.
+ ///
+ internal class BundleStream : Stream
+ {
+ readonly ArcViewStream m_input;
+ readonly long m_length;
+ IList m_segments;
+ long m_position;
+ int m_current_segment;
+ byte[] m_buffer;
+ int m_buffer_pos;
+ int m_buffer_len;
+ byte[] m_packed;
+
+ public BundleStream (ArcView file, IList segments)
+ {
+ if (null == segments || 0 == segments.Count)
+ throw new ArgumentException ("Segments list is empty.", "segments");
+ m_input = file.CreateStream();
+ m_segments = segments;
+ var last_segment = m_segments[m_segments.Count-1];
+ m_length = last_segment.UnpackedOffset + last_segment.UnpackedSize;
+ m_position = 0;
+ m_current_segment = 0;
+ m_input.Position = m_segments[0].Offset;
+ }
+
+ public override bool CanRead { get { return !m_disposed; } }
+ public override bool CanSeek { get { return !m_disposed; } }
+ public override bool CanWrite { get { return false; } }
+ public override long Length { get { return m_length; } }
+ public override long Position
+ {
+ get { return m_position; }
+ set {
+ if (value == m_position)
+ return;
+ if (value < 0)
+ throw new ArgumentOutOfRangeException ("value", "Stream position is out of range.");
+ m_position = value;
+ int segment_index = 0;
+ for (int i = 1; i < m_segments.Count; ++i)
+ {
+ if (m_segments[i].UnpackedOffset > value)
+ break;
+ ++segment_index;
+ }
+ var segment = m_segments[segment_index];
+ if (segment_index != m_current_segment)
+ {
+ m_current_segment = segment_index;
+ m_buffer_len = 0;
+ }
+ if (segment.IsCompressed)
+ {
+ m_buffer_pos = (int)(m_position - segment.UnpackedOffset);
+ }
+ else
+ {
+ m_buffer_pos = 0;
+ m_input.Position = segment.Offset + (m_position - segment.UnpackedOffset);
+ }
+ }
+ }
+
+ void ReadCompressedSegment (BundleSegment segment)
+ {
+ m_input.Position = segment.Offset;
+ if (null == m_packed || segment.PackedSize > m_packed.Length)
+ m_packed = new byte[segment.PackedSize];
+ int packed_size = m_input.Read (m_packed, 0, (int)segment.PackedSize);
+ if (null == m_buffer || segment.UnpackedSize > m_buffer.Length)
+ m_buffer = new byte[segment.UnpackedSize];
+ if (3 == segment.Compression)
+ m_buffer_len = Lz4Compressor.DecompressBlock (m_packed, packed_size, m_buffer, (int)segment.UnpackedSize);
+ else
+ throw new NotImplementedException ("Not supported Unity asset bundle compression.");
+ }
+
+ int ReadFromSegment (BundleSegment segment, byte[] buffer, int offset, int count)
+ {
+ Debug.Assert (m_position >= segment.UnpackedOffset && m_position <= segment.UnpackedOffset + segment.UnpackedSize);
+ if (!segment.IsCompressed)
+ {
+ int available = (int)Math.Min (count, (segment.UnpackedOffset + segment.UnpackedSize) - m_position);
+ return m_input.Read (buffer, offset, available);
+ }
+ else
+ {
+ if (0 == m_buffer_len)
+ ReadCompressedSegment (segment);
+ int available = Math.Min (count, m_buffer_len - m_buffer_pos);
+ Buffer.BlockCopy (m_buffer, m_buffer_pos, buffer, offset, available);
+ m_buffer_pos += available;
+ return available;
+ }
+ }
+
+ public override int Read (byte[] buffer, int offset, int count)
+ {
+ if (m_position >= m_length)
+ return 0;
+ int total_read = 0;
+ while (count > 0)
+ {
+ var segment = m_segments[m_current_segment];
+ int read = ReadFromSegment (segment, buffer, offset, count);
+ m_position += read;
+ total_read += read;
+ offset += read;
+ count -= read;
+ if (count > 0)
+ {
+ if (m_current_segment+1 == m_segments.Count)
+ break;
+ ++m_current_segment;
+ m_buffer_len = m_buffer_pos = 0;
+ m_input.Position = m_segments[m_current_segment].Offset;
+ Debug.Assert (m_position == m_segments[m_current_segment].UnpackedOffset);
+ }
+ }
+ return total_read;
+ }
+
+ public override void Flush()
+ {
+ }
+
+ public override long Seek (long offset, SeekOrigin origin)
+ {
+ switch (origin)
+ {
+ case SeekOrigin.Current: offset += m_position; break;
+ case SeekOrigin.End: offset += m_length; break;
+ }
+ Position = offset;
+ return offset;
+ }
+
+ public override void SetLength (long length)
+ {
+ throw new NotSupportedException ("Stream.SetLength method is not supported.");
+ }
+
+ public override void Write (byte[] buffer, int offset, int count)
+ {
+ throw new NotSupportedException ("Stream.Write method is not supported.");
+ }
+
+ public override void WriteByte (byte value)
+ {
+ throw new NotSupportedException ("Stream.WriteByte method is not supported.");
+ }
+
+ bool m_disposed = false;
+ protected override void Dispose (bool disposing)
+ {
+ if (!m_disposed)
+ {
+ if (disposing)
+ m_input.Dispose();
+ m_disposed = true;
+ base.Dispose (disposing);
+ }
+ }
+ }
+}
diff --git a/Experimental/Unity/OggStream.cs b/Experimental/Unity/OggStream.cs
new file mode 100644
index 00000000..37309a04
--- /dev/null
+++ b/Experimental/Unity/OggStream.cs
@@ -0,0 +1,416 @@
+//! \file OggStream.cs
+//! \date Sat Apr 08 01:43:58 2017
+//! \brief libogg partial implementation.
+//
+// Copyright (C) 2017 by morkt
+//
+// 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.
+//
+
+using System;
+using System.IO;
+using System.Text;
+using GameRes.Utility;
+
+namespace GameRes.Formats.Vorbis
+{
+ internal sealed class OggBitStream : IDisposable
+ {
+ LsbBitStream m_input;
+
+ public OggBitStream (OggPacket input)
+ {
+ // certainly an overhead to create a new stream for every packet, but it's so convenient
+ var buf = new MemoryStream (input.Packet);
+ m_input = new LsbBitStream (buf);
+ }
+
+ /// Read bits from a stream.
+ /// -1 if there was not enough bits in a stream
+ public int ReadBits (int count)
+ {
+ if (count <= 24)
+ return m_input.GetBits (count);
+ else if (count > 32)
+ throw new ArgumentOutOfRangeException ("count", "Attempted to read more than 32 bits from OggBitStream.");
+ int lo = m_input.GetBits (24);
+ return m_input.GetBits (count - 24) << 24 | lo;
+ }
+
+ /// Read 8-bit integer from bitstream.
+ /// -1 if there was not enough bits in a stream
+ public int ReadByte ()
+ {
+ return ReadBits (8);
+ }
+
+ /// Read 8-bit integer from bitstream.
+ /// Thrown if there's not enough bits in a stream.
+ public byte ReadUInt8 ()
+ {
+ int b = ReadBits (8);
+ if (-1 == b)
+ throw new EndOfStreamException();
+ return (byte)b;
+ }
+
+ /// Read 32-bit integer from bitstream.
+ /// Thrown if there's not enough bits in a stream.
+ public int ReadInt32 ()
+ {
+ int lo = ReadBits (16);
+ int hi = ReadBits (16);
+ if (-1 == lo || -1 == hi)
+ throw new EndOfStreamException();
+ return hi << 16 | lo;
+ }
+
+ /// Attempt to read bytes from stream.
+ /// Thrown if there's not enough bytes in a bitstream.
+ public byte[] ReadBytes (int count)
+ {
+ var buf = new byte[count];
+ for (int i = 0; i < count; ++i)
+ buf[i] = ReadUInt8();
+ return buf;
+ }
+
+ bool m_disposed = false;
+ public void Dispose ()
+ {
+ if (!m_disposed)
+ {
+ m_input.Dispose();
+ m_disposed = true;
+ }
+ }
+ }
+
+ // struct ogg_packet
+ // https://xiph.org/ogg/doc/libogg/ogg_packet.html
+ internal class OggPacket
+ {
+ public byte[] Packet;
+ public bool BoS;
+ public bool EoS;
+
+ public long GranulePos;
+ public long PacketNo;
+
+ public void SetPacket (long packet_no, byte[] packet)
+ {
+ PacketNo = packet_no;
+ Packet = packet;
+ }
+ }
+
+ // struct ogg_stream_state
+ // https://xiph.org/ogg/doc/libogg/ogg_stream_state.html
+ internal class OggStreamState
+ {
+ byte[] BodyData; // bytes from packet bodies
+ int BodyStorage; // storage elements allocated
+ int BodyFill; // elements stored; fill mark
+ int BodyReturned; // elements of fill returned
+
+ int[] LacingVals; // The values that will go to the segment table granulepos values for headers.
+ long[] GranuleVals; // Not compact this way, but it is simple coupled to the lacing fifo.
+ int LacingStorage;
+ int LacingFill;
+
+ byte[] Header; // working space for header encode
+ int HeaderFill;
+
+ bool EoS; // set when we have buffered the last packet in the logical bitstream
+ bool BoS; // set after we've written the initial page of a logical bitstream
+ int SerialNo;
+ int PageNo;
+ long PacketNo; // sequence number for decode; the framing knows where there's a hole in the data,
+ // but we need coupling so that the codec (which is in a seperate abstraction
+ // layer) also knows about the gap
+ long GranulePos;
+
+ // https://xiph.org/ogg/doc/libogg/ogg_stream_init.html
+ public OggStreamState (int serial_no)
+ {
+ BodyStorage = 0x4000;
+ LacingStorage = 0x400;
+
+ BodyData = new byte[BodyStorage];
+ LacingVals = new int[LacingStorage];
+ GranuleVals = new long[LacingStorage];
+ Header = new byte[282];
+
+ SerialNo = serial_no;
+ }
+
+ public void Clear ()
+ {
+ BodyStorage = 0;
+ BodyFill = 0;
+ BodyReturned = 0;
+ LacingStorage = 0;
+ LacingFill = 0;
+ HeaderFill = 0;
+ EoS = false;
+ BoS = false;
+ SerialNo = 0;
+ PageNo = 0;
+ PacketNo = 0;
+ GranulePos = 0;
+ }
+
+ public bool PacketIn (OggPacket op)
+ {
+ int bytes = op.Packet.Length;
+ int lacing_vals = bytes / 255 + 1;
+
+ if (BodyReturned > 0)
+ {
+ // advance packet data according to the body_returned pointer.
+ // We had to keep it around to return a pointer into the buffer last call.
+
+ BodyFill -= BodyReturned;
+ if (BodyFill > 0)
+ Buffer.BlockCopy (BodyData, BodyReturned, BodyData, 0, BodyFill);
+ BodyReturned = 0;
+ }
+
+ // make sure we have the buffer storage
+ if(!BodyExpand (bytes) || !LacingExpand (lacing_vals))
+ return false;
+
+ // Copy in the submitted packet.
+ Buffer.BlockCopy (op.Packet, 0, BodyData, BodyFill, op.Packet.Length);
+ BodyFill += op.Packet.Length;
+
+ // Store lacing vals for this packet
+ int i;
+ for (i = 0; i < lacing_vals-1; ++i)
+ {
+ LacingVals[LacingFill + i] = 0xFF;
+ GranuleVals[LacingFill + i] = GranulePos;
+ }
+ LacingVals[LacingFill + i] = bytes % 0xFF;
+ GranulePos = GranuleVals[LacingFill+i] = GranulePos;
+
+ // flag the first segment as the beginning of the packet
+ LacingVals[LacingFill] |= 0x100;
+
+ LacingFill += lacing_vals;
+ PacketNo++;
+ EoS = op.EoS;
+
+ return true;
+ }
+
+ public void Write (Stream output)
+ {
+ var page = new OggPage();
+ while (PageOut (page))
+ {
+ output.Write (page.Header, 0, page.HeaderLength);
+ output.Write (page.Body, page.BodyStart, page.BodyLength);
+ }
+ }
+
+ public void Flush (Stream output)
+ {
+ var page = new OggPage();
+ while (Flush (page, true, 0x1000))
+ {
+ output.Write (page.Header, 0, page.HeaderLength);
+ output.Write (page.Body, page.BodyStart, page.BodyLength);
+ }
+ }
+
+ public bool PageOut (OggPage page)
+ {
+ bool force = EoS && (LacingFill > 0) || (LacingFill > 0 && !BoS);
+ return Flush (page, force, 0x1000);
+ }
+
+ bool BodyExpand (int needed)
+ {
+ if (BodyStorage - needed <= BodyFill)
+ {
+ if (BodyStorage > int.MaxValue - needed)
+ {
+ Clear();
+ return false;
+ }
+ int body_storage = BodyStorage + needed;
+ if (body_storage < int.MaxValue - 1024)
+ body_storage += 1024;
+ Array.Resize (ref BodyData, body_storage);
+ BodyStorage = body_storage;
+ }
+ return true;
+ }
+
+ bool LacingExpand (int needed)
+ {
+ if (LacingStorage - needed <= LacingFill)
+ {
+ if (LacingStorage > int.MaxValue - needed)
+ {
+ Clear();
+ return false;
+ }
+ int lacing_storage = LacingStorage + needed;
+ if (lacing_storage < int.MaxValue - 32)
+ lacing_storage += 32;
+ Array.Resize (ref LacingVals, lacing_storage);
+ Array.Resize (ref GranuleVals, lacing_storage);
+ LacingStorage = lacing_storage;
+ }
+ return true;
+ }
+
+ bool Flush (OggPage og, bool force, int fill)
+ {
+ int maxvals = Math.Min (LacingFill, 0xFF);
+ if (0 == maxvals)
+ return false;
+
+ // construct a page
+ // decide how many segments to include
+
+ int vals = 0;
+ int acc = 0;
+ long granule_pos = -1;
+
+ // If this is the initial header case, the first page must only include
+ // the initial header packet
+ if (!BoS) // 'initial header page' case
+ {
+ granule_pos = 0;
+ for (vals = 0; vals < maxvals; vals++)
+ {
+ if ((LacingVals[vals] & 0xFF) < 0xFF)
+ {
+ vals++;
+ break;
+ }
+ }
+ }
+ else
+ {
+ int packets_done = 0;
+ int packet_just_done = 0;
+ for (vals = 0; vals < maxvals; vals++)
+ {
+ if (acc > fill && packet_just_done >= 4)
+ {
+ force = true;
+ break;
+ }
+ acc += LacingVals[vals] & 0xFF;
+ if ((LacingVals[vals] & 0xFF) < 0xFF)
+ {
+ granule_pos = GranuleVals[vals];
+ packet_just_done = ++packets_done;
+ }
+ else
+ packet_just_done = 0;
+ }
+ if (0xFF == vals)
+ force = true;
+ }
+ if (!force)
+ return false;
+
+ // construct the header in temp storage
+ Encoding.ASCII.GetBytes ("OggS", 0, 4, Header, 0);
+
+ // stream structure version
+ Header[4] = 0;
+
+ // continued packet flag?
+ Header[5] = 0;
+ if ((LacingVals[0] & 0x100) == 0)
+ Header[5] |= 1;
+ // first page flag?
+ if (!BoS)
+ Header[5] |= 2;
+ // last page flag?
+ if (EoS && LacingFill == vals)
+ Header[5] |= 4;
+ BoS = true;
+
+ // 64 bits of PCM position
+ LittleEndian.Pack (granule_pos, Header, 6);
+
+ // 32 bits of stream serial number
+ LittleEndian.Pack (SerialNo, Header, 14);
+
+ // 32 bits of page counter (we have both counter and page header because this
+ // val can roll over)
+ if (-1 == PageNo)
+ PageNo = 0;
+ LittleEndian.Pack (PageNo, Header, 18);
+ ++PageNo;
+
+ int bytes = 0;
+ // segment table
+ Header[26] = (byte)vals;
+ for (int i = 0; i < vals; ++i)
+ bytes += Header[i+27] = (byte)LacingVals[i];
+
+ // set pointers in the ogg_page struct
+ og.Header = Header;
+ og.HeaderLength = HeaderFill = vals + 27;
+ og.Body = BodyData;
+ og.BodyStart = BodyReturned;
+ og.BodyLength = bytes;
+
+ // advance the lacing data and set the body_returned pointer
+ LacingFill -= vals;
+ Array.Copy (LacingVals, vals, LacingVals, 0, LacingFill);
+ Array.Copy (GranuleVals, vals, GranuleVals, 0, LacingFill);
+ BodyReturned += bytes;
+
+ // calculate the checksum
+ og.SetChecksum();
+
+ return true;
+ }
+ }
+
+ // struct ogg_page
+ // https://xiph.org/ogg/doc/libogg/ogg_page.html
+ internal class OggPage
+ {
+ public byte[] Header;
+ public int HeaderLength;
+ public byte[] Body;
+ public int BodyStart;
+ public int BodyLength;
+
+ public void SetChecksum ()
+ {
+ Header[22] = Header[23] = Header[24] = Header[25] = 0;
+
+ uint crc = Crc32Normal.UpdateCrc (0, Header, 0, HeaderLength);
+ crc = Crc32Normal.UpdateCrc (crc, Body, BodyStart, BodyLength);
+
+ LittleEndian.Pack (crc, Header, 22);
+ }
+ }
+}
diff --git a/Experimental/Unity/Vorbis.cs b/Experimental/Unity/Vorbis.cs
new file mode 100644
index 00000000..3360d4c4
--- /dev/null
+++ b/Experimental/Unity/Vorbis.cs
@@ -0,0 +1,830 @@
+//! \file Vorbis.cs
+//! \date Fri Apr 07 21:07:30 2017
+//! \brief partial libvorbis port.
+//
+// Only parts crucial for FSB5 decoding got implemented.
+// Parts that are not used in FSB5 decoder are left out completely or commented out.
+//
+// Copyright (C) 2017 by morkt
+//
+// 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.
+// -----------------------------------------------------------------------------
+//
+// libvorbis Copyright (c) 2002-2008 Xiph.org Foundation
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions
+// are met:
+//
+// - Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//
+// - Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+//
+// - Neither the name of the Xiph.org Foundation nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION
+// OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+//
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+
+namespace GameRes.Formats.Vorbis
+{
+ // struct vorbis_info
+ // https://xiph.org/vorbis/doc/libvorbis/vorbis_info.html
+ class VorbisInfo
+ {
+ public int Version;
+ public int Channels;
+ public int Rate;
+ public int BitrateUpper;
+ public int BitrateNominal;
+ public int BitrateLower;
+
+ public CodecSetupInfo CodecSetup = new CodecSetupInfo();
+
+ const int TransormB = 1;
+ const int WindowB = 1;
+ const int TimeB = 1;
+ const int FloorB = 2;
+ const int ResB = 3;
+ const int MapB = 1;
+
+ Func[] FloorMethods;
+
+ public VorbisInfo ()
+ {
+ FloorMethods = new Func[] {
+ UnpackFloor0,
+ UnpackFloor1
+ };
+ }
+
+ // https://xiph.org/vorbis/doc/libvorbis/vorbis_synthesis_headerin.html
+ public void SynthesisHeaderin (VorbisComment vc, OggPacket op)
+ {
+ using (var input = new OggBitStream (op))
+ {
+ int packtype = input.ReadUInt8();
+ var buf = input.ReadBytes (6);
+ if (!buf.AsciiEqual ("vorbis"))
+ throw new InvalidDataException ("Not an Ogg/Vorbis stream.");
+ switch (packtype)
+ {
+ case 1:
+ if (!op.BoS)
+ throw InvalidHeader();
+ if (Rate != 0)
+ throw InvalidHeader();
+ UnpackInfo (input);
+ break;
+
+ case 3:
+ if (0 == Rate)
+ throw InvalidHeader();
+ vc.UnpackComment (input);
+ break;
+
+ case 5:
+ if (0 == Rate || null == vc.Vendor)
+ throw InvalidHeader();
+ UnpackBooks (input);
+ break;
+
+ default:
+ throw InvalidHeader();
+ }
+ }
+ }
+
+ internal static int iLog (uint num)
+ {
+ int bits = 0;
+ while (num != 0)
+ {
+ ++bits;
+ num >>= 1;
+ }
+ return bits;
+ }
+
+ internal static int CountBits (uint num)
+ {
+ int bits = 0;
+ if (num != 0)
+ --num;
+ while (num != 0)
+ {
+ ++bits;
+ num >>= 1;
+ }
+ return bits;
+ }
+
+ ///
+ /// Count number of bits set in .
+ ///
+ internal static int CountSetBits (uint num)
+ {
+ int bits = 0;
+ while (num != 0)
+ {
+ bits += (int)num & 1;
+ num >>= 1;
+ }
+ return bits;
+ }
+
+ // https://www.xiph.org/vorbis/doc/libvorbis/vorbis_packet_blocksize.html
+ public int PacketBlockSize (OggPacket op)
+ {
+ using (var input = new OggBitStream (op))
+ {
+ // Check the packet type
+ if (input.ReadBits (1) != 0)
+ throw new InvalidDataException ("Not an audio data packet.");
+
+ int modebits = 0;
+ for (int v = CodecSetup.Modes; v > 1; v >>= 1)
+ {
+ modebits++;
+ }
+
+ // read our mode and pre/post windowsize
+ int mode = input.ReadBits (modebits);
+
+ if (-1 == mode)
+ throw new InvalidDataException ("Invalid Ogg/Vorbis packet.");
+
+ return CodecSetup.BlockSizes[CodecSetup.ModeParam[mode].BlockFlag];
+ }
+ }
+
+ void UnpackInfo (OggBitStream input)
+ {
+ Version = input.ReadInt32();
+ if (Version != 0)
+ throw new InvalidDataException ("Invalid Vorbis encoder version.");
+ Channels = input.ReadUInt8();
+ Rate = input.ReadInt32();
+
+ BitrateUpper = input.ReadInt32();
+ BitrateNominal = input.ReadInt32();
+ BitrateLower = input.ReadInt32();
+
+ CodecSetup.BlockSizes[0] = 1 << input.ReadBits (4);
+ CodecSetup.BlockSizes[1] = 1 << input.ReadBits (4);
+
+ if (input.ReadBits (1) != 1)
+ throw InvalidHeader();
+ }
+
+ void UnpackBooks (OggBitStream input)
+ {
+ // codebooks
+ CodecSetup.Books = input.ReadUInt8() + 1;
+ if (CodecSetup.Books <= 0)
+ throw InvalidHeader();
+
+ for (int i = 0; i < CodecSetup.Books; ++i)
+ {
+ var param = StaticBookUnpack (input);
+ if (null == param)
+ throw InvalidHeader();
+ CodecSetup.BookParam[i] = param;
+ }
+
+ // time backend settings; hooks are unused
+ int times = input.ReadBits (6) + 1;
+ if (times <= 0)
+ throw InvalidHeader();
+ for (int i = 0; i < times; ++i)
+ {
+ int test = input.ReadBits (16);
+ if (test < 0 || test >= TimeB)
+ throw InvalidHeader();
+ }
+
+ // floor backend settings
+ CodecSetup.Floors = input.ReadBits (6) + 1;
+ if (CodecSetup.Floors <= 0)
+ throw InvalidHeader();
+ for (int i = 0; i < CodecSetup.Floors; i++)
+ {
+ int floor_type = input.ReadBits (16);
+ if (floor_type < 0 || floor_type >= FloorB)
+ throw InvalidHeader();
+ CodecSetup.FloorType[i] = floor_type;
+ var param = FloorMethods[floor_type] (input);
+ if (null == param)
+ throw InvalidHeader();
+ CodecSetup.FloorParam[i] = param;
+ }
+
+ // residue backend settings
+ CodecSetup.Residues = input.ReadBits (6) + 1;
+ if (CodecSetup.Residues <= 0)
+ throw InvalidHeader();
+ for (int i = 0; i < CodecSetup.Residues; ++i)
+ {
+ int residue_type = input.ReadBits (16);
+ if (residue_type < 0 || residue_type >= ResB)
+ throw InvalidHeader();
+ CodecSetup.ResidueType[i] = residue_type;
+ var param = UnpackResidue (input);
+ if (null == param)
+ throw InvalidHeader();
+ CodecSetup.ResidueParam[i] = param;
+ }
+
+ // map backend settings
+ CodecSetup.Maps = input.ReadBits (6) + 1;
+ if (CodecSetup.Maps <= 0)
+ throw InvalidHeader();
+ for (int i = 0; i < CodecSetup.Maps; ++i)
+ {
+ int map_type = input.ReadBits (16);
+ if (map_type < 0 || map_type >= MapB)
+ throw InvalidHeader();
+ CodecSetup.MapType[i] = map_type;
+ var param = UnpackMapping (input);
+ if (null == param)
+ throw InvalidHeader();
+ CodecSetup.MapParam[i] = param;
+ }
+
+ // mode settings
+ CodecSetup.Modes = input.ReadBits (6) + 1;
+ if (CodecSetup.Modes <= 0)
+ throw InvalidHeader();
+ for (int i = 0; i < CodecSetup.Modes; ++i)
+ {
+ CodecSetup.ModeParam[i].BlockFlag = input.ReadBits (1);
+ CodecSetup.ModeParam[i].WindowType = input.ReadBits (16);
+ CodecSetup.ModeParam[i].TransformType = input.ReadBits (16);
+ CodecSetup.ModeParam[i].Mapping = input.ReadBits (8);
+
+ if (CodecSetup.ModeParam[i].WindowType >= WindowB ||
+ CodecSetup.ModeParam[i].TransformType >= WindowB ||
+ CodecSetup.ModeParam[i].Mapping >= CodecSetup.Maps ||
+ CodecSetup.ModeParam[i].Mapping < 0)
+ throw InvalidHeader();
+ }
+ if (input.ReadBits (1) != 1)
+ throw InvalidHeader();
+ }
+
+ StaticCodebook StaticBookUnpack (OggBitStream input)
+ {
+ // make sure alignment is correct
+ if (input.ReadBits (24) != 0x564342)
+ return null;
+
+ var s = new StaticCodebook();
+
+ // first the basic parameters
+ s.dim = input.ReadBits (16);
+ s.entries = input.ReadBits (24);
+ if (-1 == s.entries)
+ return null;
+
+ if (iLog ((uint)s.dim) + iLog ((uint)s.entries) > 24)
+ return null;
+
+ // codeword ordering.... length ordered or unordered?
+ switch (input.ReadBits (1))
+ {
+ case 0:
+ // allocated but unused entries?
+ int unused = input.ReadBits (1);
+ // unordered
+ s.lengthlist = new byte[s.entries];
+
+ // allocated but unused entries?
+ if (unused > 0)
+ {
+ // yes, unused entries
+ for(int i = 0; i < s.entries; ++i)
+ {
+ if (input.ReadBits (1) > 0)
+ {
+ int num = input.ReadBits (5);
+ if (-1 == num)
+ return null;
+ s.lengthlist[i] = (byte)(num+1);
+ }
+ else
+ s.lengthlist[i] = 0;
+ }
+ }
+ else
+ {
+ // all entries used; no tagging
+ for (int i = 0; i < s.entries; ++i)
+ {
+ int num = input.ReadBits (5);
+ if (-1 == num)
+ return null;
+ s.lengthlist[i] = (byte)(num+1);
+ }
+ }
+ break;
+
+ case 1: // ordered
+ int length = input.ReadBits (5) + 1;
+ if (0 == length)
+ return null;
+ s.lengthlist = new byte[s.entries];
+
+ for (int i = 0; i < s.entries; )
+ {
+ int num = input.ReadBits (iLog ((uint)(s.entries-i)));
+ if (-1 == num || length > 32 || num > s.entries-i
+ || (num > 0 && ((num-1) >> (length-1)) > 1))
+ return null;
+ for (int j = 0; j < num; ++j, ++i)
+ s.lengthlist[i] = (byte)length;
+ length++;
+ }
+ break;
+
+ default:
+ return null;
+ }
+
+ // Do we have a mapping to unpack?
+ switch((s.maptype = input.ReadBits (4)))
+ {
+ case 0: // no mapping
+ break;
+
+ case 1: case 2:
+ // implicitly populated value mapping
+ // explicitly populated value mapping
+
+ s.q_min = input.ReadInt32();
+ s.q_delta = input.ReadInt32();
+ s.q_quant = input.ReadBits (4) + 1;
+ s.q_sequencep = input.ReadBits (1);
+ if (-1 == s.q_sequencep)
+ return null;
+ int quantvals = 0;
+ switch (s.maptype)
+ {
+ case 1:
+ quantvals = s.dim == 0 ? 0 : s.Maptype1Quantvals();
+ break;
+ case 2:
+ quantvals = s.entries * s.dim;
+ break;
+ }
+
+ // quantized values
+ s.quantlist = new int[quantvals];
+ for (int i = 0; i < quantvals; ++i)
+ s.quantlist[i] = input.ReadBits (s.q_quant);
+
+ if (quantvals > 0 && s.quantlist[quantvals-1] == -1)
+ return null;
+ break;
+
+ default: // EOF
+ return null;
+ }
+
+ // all set
+ return s;
+ }
+
+ VorbisInfoFloor UnpackFloor0 (OggBitStream input)
+ {
+ var info = new VorbisInfoFloor();
+ int order = input.ReadBits (8);
+ int rate = input.ReadBits (16);
+ int barkmap = input.ReadBits (16);
+ int ampbits = input.ReadBits (6);
+ int ampdB = input.ReadBits (8);
+ int numbooks = input.ReadBits (4) + 1;
+
+ if (order < 1 || rate < 1 || barkmap < 1 || numbooks < 1)
+ return null;
+
+ for (int j = 0; j < numbooks; ++j)
+ {
+ int books = input.ReadByte();
+ if (books < 0 || books >= CodecSetup.Books)
+ return null;
+ }
+ return info;
+ }
+
+ VorbisInfoFloor UnpackFloor1 (OggBitStream input)
+ {
+ int max_class = -1;
+
+ var info = new VorbisInfoFloor();
+ // read partitions
+ int partitions = input.ReadBits (5); // only 0 to 31 legal
+ var partition_class = new int[partitions];
+ for (int j = 0; j < partitions; ++j)
+ {
+ partition_class[j] = input.ReadBits (4); // only 0 to 15 legal
+ if (partition_class[j] < 0)
+ return null;
+ if (max_class < partition_class[j])
+ max_class = partition_class[j];
+ }
+
+ // read partition classes
+ var class_dim = new int[max_class+1];
+ for (int j = 0; j < max_class+1; ++j)
+ {
+ class_dim[j] = input.ReadBits (3) + 1; // 1 to 8
+ int class_subs = input.ReadBits (2); // 0,1,2,3 bits
+ if (class_subs < 0)
+ return null;
+ if (class_subs > 0)
+ input.ReadBits (8); // class_book
+ for (int k = 0; k < (1 << class_subs); ++k)
+ {
+ int class_subbook = input.ReadBits (8) - 1; // info.class_subbook[j][k]
+ }
+ }
+
+ // read the post list
+ int mult = input.ReadBits (2) + 1; // only 1,2,3,4 legal now
+ int rangebits = input.ReadBits (4);
+ if (rangebits < 0)
+ return null;
+
+ int count = 0;
+// var postlist = new int[VorbisInfoFloor.Posit + 2];
+ for (int j = 0, k = 0; j < partitions; ++j)
+ {
+ count += class_dim[partition_class[j]];
+ if (count > VorbisInfoFloor.Posit)
+ return null;
+ for (; k < count; ++k)
+ {
+ int t = input.ReadBits (rangebits);
+ if (t < 0 || t >= (1 << rangebits))
+ return null;
+// postlist[k+2] = t;
+ }
+ }
+// postlist[0] = 0;
+// postlist[1] = 1< postlist[i]).ToArray();
+ for (int j = 1; j < count+2; j++)
+ if(postlist[indices[j-1]] == postlist[indices[j]])
+ return null;
+ */
+ return info;
+ }
+
+ object UnpackResidue (OggBitStream input)
+ {
+ var info = new VorbisInfoResidue();
+
+ info.begin = input.ReadBits (24);
+ info.end = input.ReadBits (24);
+ info.grouping = input.ReadBits (24) + 1;
+ info.partitions = input.ReadBits (6) + 1;
+ info.groupbook = input.ReadBits (8);
+
+ // check for premature EOP
+ if (info.groupbook < 0)
+ return null;
+
+ int acc = 0;
+ for (int j = 0; j < info.partitions; ++j)
+ {
+ int cascade = input.ReadBits (3);
+ int cflag = input.ReadBits (1);
+ if (cflag < 0)
+ return null;
+ if (cflag > 0)
+ {
+ int c = input.ReadBits (5);
+ if (c < 0)
+ return null;
+ cascade |= c << 3;
+ }
+ // info.secondstages[j] = cascade;
+
+ acc += CountSetBits ((uint)cascade);
+ }
+ for (int j = 0; j < acc; ++j)
+ {
+ int book = input.ReadBits (8);
+ if (book < 0)
+ return null;
+ if (book >= CodecSetup.Books)
+ return null;
+// if (CodecSetup.book_param[book].maptype == 0) return null;
+// info.booklist[j] = book;
+ }
+ if (info.groupbook >= CodecSetup.Books)
+ return null;
+
+ /*
+ int entries = CodecSetup.book_param[info.groupbook].entries;
+ int dim = CodecSetup.book_param[info.groupbook].dim;
+ int partvals = 1;
+ if (dim < 1)
+ return null;
+ while (dim > 0)
+ {
+ partvals *= info.partitions;
+ if (partvals > entries)
+ return null;
+ dim--;
+ }
+ info.partvals = partvals;
+ */
+ return info;
+ }
+
+ VorbisInfoMapping UnpackMapping (OggBitStream input)
+ {
+ var info = new VorbisInfoMapping();
+
+ int b = input.ReadBits (1);
+ if (b < 0)
+ return null;
+ if (b > 0)
+ {
+ info.submaps = input.ReadBits (4) + 1;
+ if (info.submaps <= 0)
+ return null;
+ }
+ else
+ info.submaps = 1;
+
+ b = input.ReadBits (1);
+ if (b < 0)
+ return null;
+ if (b > 0)
+ {
+ info.coupling_steps = input.ReadBits (8) + 1;
+ if (info.coupling_steps <= 0)
+ return null;
+ for (int i = 0; i < info.coupling_steps; ++i)
+ {
+ int bits = CountBits ((uint)Channels);
+ int testM = input.ReadBits (bits);
+ int testA = input.ReadBits (bits);
+ if (testM < 0 || testA < 0 || testM == testA
+ || testM >= Channels || testA >= Channels)
+ return null;
+ }
+ }
+
+ if (input.ReadBits (2) != 0)
+ return null;
+
+ if (info.submaps > 1)
+ {
+ for (int i = 0; i < Channels; ++i)
+ {
+ int chmuxlist = input.ReadBits (4);
+ if (chmuxlist >= info.submaps || chmuxlist < 0)
+ return null;
+ }
+ }
+ for (int i = 0; i < info.submaps; ++i)
+ {
+ input.ReadByte(); // time submap unused
+ int floorsubmap = input.ReadByte();
+ if (floorsubmap >= CodecSetup.Floors || floorsubmap < 0)
+ return null;
+ int residuesubmap = input.ReadByte();
+ if (residuesubmap >= CodecSetup.Residues || residuesubmap < 0)
+ return null;
+ }
+ return info;
+ }
+
+ internal static InvalidDataException InvalidHeader ()
+ {
+ return new InvalidDataException ("Invalid header in Ogg/Vorbis stream.");
+ }
+ }
+
+ // struct vorbis_comment
+ // https://xiph.org/vorbis/doc/libvorbis/vorbis_comment.html
+ class VorbisComment
+ {
+ public List Comments;
+ public byte[] Vendor;
+
+ internal static readonly byte[] EncodeVendorString = Encoding.UTF8.GetBytes ("mørkt GARbro 20170407");
+
+ public VorbisComment ()
+ {
+ Comments = new List();
+ }
+
+ // https://xiph.org/vorbis/doc/libvorbis/vorbis_commentheader_out.html
+ public void HeaderOut (OggPacket packet)
+ {
+ using (var buf = new MemoryStream())
+ using (var output = new BinaryWriter (buf))
+ {
+ // preamble
+ output.Write ((byte)3);
+ output.Write ("vorbis".ToCharArray());
+
+ // vendor
+ output.Write (EncodeVendorString.Length);
+ output.Write (EncodeVendorString);
+
+ // comments
+ output.Write (Comments.Count);
+ foreach (var comment in Comments)
+ {
+ if (comment != null && comment.Length > 0)
+ {
+ output.Write (comment.Length);
+ output.Write (comment);
+ }
+ else
+ {
+ output.Write (0);
+ }
+ }
+ output.Write ((byte)1);
+ output.Flush();
+
+ packet.SetPacket (1, buf.ToArray());
+ packet.BoS = false;
+ packet.EoS = false;
+ packet.GranulePos = 0;
+ }
+ }
+
+ internal void UnpackComment (OggBitStream input)
+ {
+ int vendor_len = input.ReadInt32();
+ if (vendor_len < 0)
+ throw VorbisInfo.InvalidHeader();
+ var vendor = input.ReadBytes (vendor_len);
+
+ int count = input.ReadInt32();
+ if (count < 0)
+ throw VorbisInfo.InvalidHeader();
+
+ var comments = new List (count);
+ for (int i = 0; i < count; ++i)
+ {
+ int len = input.ReadInt32();
+ if (len < 0)
+ throw VorbisInfo.InvalidHeader();
+ var bytes = input.ReadBytes (len);
+ comments.Add (bytes);
+ }
+ if (input.ReadBits (1) != 1)
+ throw VorbisInfo.InvalidHeader();
+
+ this.Vendor = vendor;
+ this.Comments = comments;
+ }
+ }
+
+ // codec_setup_info
+ class CodecSetupInfo
+ {
+ public int[] BlockSizes = new int[2];
+
+ public int Modes;
+ public int Maps;
+ public int Floors;
+ public int Residues;
+ public int Books;
+
+ internal VorbisInfoMode[] ModeParam = new VorbisInfoMode[64];
+ internal int[] ResidueType = new int[64];
+ internal object[] ResidueParam = new object[64];
+ internal int[] FloorType = new int[64];
+ internal object[] FloorParam = new object[64];
+ internal int[] MapType = new int[64];
+ internal VorbisInfoMapping[] MapParam = new VorbisInfoMapping[64];
+ internal StaticCodebook[] BookParam = new StaticCodebook[256];
+ }
+
+ // struct vorbis_info_mode
+ struct VorbisInfoMode
+ {
+ public int BlockFlag;
+ public int WindowType;
+ public int TransformType;
+ public int Mapping;
+ }
+
+ // struct static_codebook
+ class StaticCodebook
+ {
+ public int dim; // codebook dimensions (elements per vector)
+ public int entries; // codebook entries
+ public byte[] lengthlist; // codeword lengths in bits
+
+ // mapping ***************************************************************
+ public int maptype; // 0=none
+ // 1=implicitly populated values from map column
+ // 2=listed arbitrary values
+
+ // The below does a linear, single monotonic sequence mapping.
+ public int q_min; // packed 32 bit float; quant value 0 maps to minval
+ public int q_delta; // packed 32 bit float; val 1 - val 0 == delta
+ public int q_quant; // bits: 0 < quant <= 16
+ public int q_sequencep; // bitflag
+
+ public int[] quantlist; // map == 1: (int)(entries^(1/dim)) element column map
+ // map == 2: list of dim*entries quantized entry vals
+
+ internal int Maptype1Quantvals ()
+ {
+ int vals = (int)Math.Floor (Math.Pow ((float)entries, 1.0f / dim));
+
+ // the above *should* be reliable, but we'll not assume that FP is
+ // ever reliable when bitstream sync is at stake; verify via integer
+ // means that vals really is the greatest value of dim for which
+ // vals^b->bim <= b->entries.
+ // treat the above as an initial guess
+ for (;;)
+ {
+ int acc = 1;
+ int acc1 = 1;
+ for (int i = 0; i < dim; ++i)
+ {
+ acc *= vals;
+ acc1 *= vals+1;
+ }
+ if (acc <= entries && acc1 > entries)
+ break;
+ if (acc > entries)
+ vals--;
+ else
+ vals++;
+ }
+ return vals;
+ }
+ }
+
+ class VorbisInfoMapping
+ {
+ public int submaps;
+ public int coupling_steps;
+ }
+
+ class VorbisInfoFloor
+ {
+ public const int Posit = 63;
+ }
+
+ class VorbisInfoResidue
+ {
+ public int begin;
+ public int end;
+ public int grouping;
+ public int partitions;
+ public int groupbook;
+ }
+}
diff --git a/Experimental/Unity/strings.dat b/Experimental/Unity/strings.dat
new file mode 100644
index 00000000..1cfa4177
Binary files /dev/null and b/Experimental/Unity/strings.dat differ