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